I have a directive for a filter object. The directive represents the filter as group of comboboxes. And I have a couple of another directives that should be affected by changes in the filter state (a list, for example). So when I change the value of a combobox in the filter it should update the property of the filter. Then somehow other directives should be informed that filter has been changed and update accordingly after that. For now I have a simple service for storing the filter state:
angular.module("services", []).factory("svcFilter", function(){
var filter = {};
return filter;
});
This service is injected in the filter directive. When value of a combobox in the filter directive changed filter state changed:
angular.module("filter-module", ["services"])
.directive("filter", function(){
return {
restrict: "E",
templateUrl: "path/to/filter/template.html",
controller: function($scope, svcFilter){
...
$scope.$watch("filterProp1", function(newValue){
svcFilter["filterProp1"] = newValue;
});
}
}
});
where filterProp1 is bound to the combobox value.
So far so good. I can inject this service into other directives and they will be having the access to the filter state but will be never notified about its changes. How to solve it? How to notify others about the changes?
I worked off of #sss fiddle to amend it for isolate scope - fiddle
template: "<select ng-model='isolateProp1' ng-change='onChange()'> <option value='volvo'>Volvo</option> <option value='saab'>Saab</option> </select> ",
controller: function ($scope, svcFilter) {
$scope.svcfltr = svcFilter; // <-- this line seems to be making the difference
$scope.$watch("svcfltr.filterProp1", function () {
$scope.isolateProp1 = svcFilter.filterProp1;
});
$scope.onChange = function(){
svcFilter.filterProp1 = $scope.isolateProp1;
};
}
EDIT:
Another approach is to directly assign a to-be-observed property of the service to a property of $scope:
$scope.isolateProp = svcFilter.filterProp;
Then, $scope.$watch is not even needed if you're using isolateProp as an expression in the view: {{isolateScope}} or ng-model="isolateScope".
Related
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'm trying to hold a global MainCtrl controller that serves the navigation menus. From time to time these menu items should be updated by various controllers.
Now I thought I might just bind the navigation links to the controller, and update the controller variable as follows:
<div ng-controller="MainCtrl">
<li ng-repeat="nav in navigations">
{{nav.label}}
</li>
</div>
<div ng-view></div> <!-- renders different controllers, eg testController.js -->
app.controller('MainCtrl', ['$scope', 'navigationService', function($scope, navigationService) {
//binding the property of the service
$scope.navigations = navigationService.navigations;
}]);
app.service('navigationService', function() {
return {
navigations: null
};
});
But, when calling the service and updating the navigations variable inside, nothing is changed in the view. Why?
angular.module('test').controller('testController', ['$scope', '$http', 'navigationService', function($scope, $http, navigationService) {
$http.get(url)
.success(function(data) {
navigationService.navigations = data.some.navigations; //assume json data exists
});
}]);
How can I achieve this two-way databinding, forcing a view update from one controller to another?
You are returning a primitive from service. A primitive doesn't have inheritance.
Return an object instead:
app.service('navigationService', function() {
var nav ={}; // object that will be returned
function init(){
$http.get(url)
.success(function(data) {
// modify object returned from service rather than reassign a primitive value
nav.items = data.some.navigations; exists
});
}
init();//make request to load the data
return { // can add more properties if needed
nav: nav
};
});
Then in controller:
$scope.navigations = navigationService.nav;
// will initially be {} and later will inherit items property
In view
<div ng-repeat="item in navigations.items">
angular internal watches will pick up the changes now made to the object and render view accordingly
After using Angular for more than 2 years, I discovered, whenever you want that functionality with multiple binding from different services/controllers/directives, ALWAYS use json property, and NEVER ovverride variable instance:
I would replace that:
$scope.navigations = navigationService.navigations;
with that:
var state = {
navigations: []
};
$scope.state = state;
state.navigations = navigationService.navigations; // i prefer such syntax
// or
$scope.state.navigations = navigationService.navigations;
Why? Probably because of Angular automatic $watch()/$watchCollection() functions, which are bind to variable changes.
You need to use the $rootscope and broadcast to and keep eye on broadcast
Say your data is changed from x controller, so here you can broadcast like this
$rootScope.$broadcast('menu-need-to-refresh');
In your main controller keep eye, like this
$scope.$on('menu-need-to-refresh', function (event, data) {
$scope.menus= service.newMenu();
});
Hope it will help you
I solved a similar problem simply by using $scope.$watch
ex:
$scope.$watch(
function(){return navigationService.navigations;},
function(newVal, oldVal){$scope.navigations = newVal;}
)
this code is not tested, but you get the gist
Update : #charlietfl + #Dmitri Algazin method is more elegant as it takes advantage of javascript itself, by using references, and avoid using two watchers in controller + ngRepeat (watchCollection in this directive will do the work).
My original answer :
You should watch for changes in the service from MainCtrl using $scope.$watch :
app.controller('MainCtrl', ['$scope', 'navigationService', function($scope, navigationService) {
//binding the property of the service
$scope.$watch(
function(){
return navigationService.navigations;
}
, function(newValue, oldValue){
if(newValue !== oldValue){
$scope.navigations = newValue;
}
})
}]);
I've got a following template:
<select
id="someSelect"
ng-model="component.picture"
ng-controller="someChildController"
size="12"
ng-options="img.url as img.name for img in image.list | filter:img.filter">
</select>
The important part is ng-model. I want to make the code as re-usable as possible, so consider this model as one that could change anytime. The question is, how to change the value from the controller, when I cannot update $scope.component.picture directly?
Is there some way to get element's model object, no matter what is it's object name?
EDIT:
I may have not been clear. Consider the case, where in different place in the application the same template is used, but with changed model (so, no component.picture). But it's still wrapped in child controller which handles the updates of the model. I cannot call component.picture directly, because I cannot be sure it's the one in ng-model.
If everything else fail, I may need to do something like:
var _el = angular.element('#someSelect');
var path = _el.attr('ng-model').split('.');
var model = $scope;
var lastIndex = -1;
path.forEach(function(p, i) {
if(typeof model[p] === 'object') {
model = model[p];
lastIndex = i;
}
else return false;
});
model[path[lastIndex+1]] = "someNewValue";
But it's quite ugly, so I wanted to know, if there's a better way to do it.
Using a directive you can inject any kind of scope objects, doesn't matter what the namespaces are, the directive will take that data as "thisdata" creating a new specific scope it can use to display the HTML starting from the template provided.
// HTML
<test-dir component="component.picture" images="image.list"></test-dir>
// JS
app.directive('testDir', function() {
return {
restrict: 'E',
scope: { component: '=', images: '=' },
template: '<select size="12" ng-options="img.url as img.name for img in images | filter:img.filter"></select>',
link: function(scope, elem, attrs) {
console.log(scope.component);
console.log(scope.images);
}
}
})
I didn't test it!
You could use a directive with a template where you pass the list of elements and the model such as:
app.directive("newDirective", function () {
return {
restrict: 'E',
scope: {
image: '=',
component: '='
},
template: '<select ng-model = "component" size = "12" ng-options = "img.url as img.name for img in image.list | filter:img.filter" > </select>'
}
});
Here the html is stored into the template which is applied every time the directive is called and it's creating a scope for the variables:
the ngModel
the object containing the list of rendered options
In this way you can a new model every time you need it with:
<new-directive image="img" component="comp.picture"></new-directive>
Where img is the object containing the list in your controller and comp is the variable where the value is stored.
I created a jsfiddle that may help you.
JSFIDDLE: https://jsfiddle.net/vaebkkg9/2/
EDIT: I changed the code so you just use component and not component.picture. If you want to assign a property of an object you have just to use it as component="comp.picture"
I'm using ui-select plugin and I'm passing ng-model from my controller to a custom directive called richSelect but the ng-model doesn't seemed to get updated on select of any item.
<richselect ng-model="dataModel"></richselect>
Custom directive
app.directive('richselect', ['$compile', function ($compile) {
return {
restrict: 'AE',
scope: {
ngModel: '=' /* Model associated with the object */
},
link: function (scope, element, attrs, ngModel) {
scope.options = [
{
'Value' : 'value1',
'Desc' : 'Value One'
},
{
'Value' : 'value2',
'Desc' : 'Value Two'
}
]
scope.getRichSelectTemplate = function () {
return '<ui-select multiple ng-model="ngModel" theme="bootstrap" ng-disabled="disabled">' +
'{{ngModel}} <ui-select-match placeholder="Select">{{$select.selected.Desc}}</ui-select-match>' +
'<ui-select-choices repeat="option in options | filter: $select.search">' +
'<span ng-bind-html="option.Desc | highlight: $select.search"></span>' +
'</ui-select-choices>' +
'</ui-select>';
}
var linkFn = $compile(scope.getRichSelectTemplate())(scope);
element.append(linkFn);
}
}
}]);
Plnkr : http://plnkr.co/edit/Im8gpxEwnU7sgrKgqZXY?p=preview
Here, try this. I wasn't exactly sure what format or output you were trying to get, but this gets the selected options passed to the View.
EDIT - I got rid of the plunker that used to be here.
You have to use the ngModel.$setViewValue in order to change the value of ng-model in the directive in the view. Additionally, to get the value of the ui-select, you need to have ng-model pointed at the options.selected
Then it was just a matter of adding an ng-click that pointed to a function that updated the view with ngModel.$setViewValue(scope.options.selected.
Also, I believe you need to `require: 'ngModel' in your directive so you can access the ngModelController.
app.directive('richselect', ['$compile', function ($compile) {
return {
restrict: 'AE',
require: 'ngModel',
scope: {
blah: '=' /* Model associated with the object */
},
link: function (scope, element, attrs, ngModel) {
scope.changer = function() {
ngModel.$setViewValue(scope.options.selected)
console.log(scope.options.selected)
}
scope.options = [
{
'Value' : 'value1',
'Desc' : 'Value One'
},
{
'Value' : 'value2',
'Desc' : 'Value Two'
}
]
scope.getRichSelectTemplate = function () {
return '<ui-select multiple ng-model="options.selected" theme="bootstrap" ng-click="changer()" ng-disabled="disabled">' +
'{{options.selected}} <ui-select-match placeholder="Select">{{$select.selected.Desc}}</ui-select-match>' +
'<ui-select-choices repeat="option in options | filter: $select.search">' +
'<span ng-bind-html="option.Desc | highlight: $select.search"></span>' +
'</ui-select-choices>' +
'</ui-select>';
}
var linkFn = $compile(scope.getRichSelectTemplate())(scope);
element.append(linkFn);
}
}
}]);
EDIT:
After a lot of digging and tinkering, per the comment below - getting two-way binding working has proved somewhat elusive. I found it was quite easy to do using the standard ui-select directive, as seen here (modified example code from ui-select), because we can easily get access to the scope of the directive:
Standard Directive Demo
I also came across a similar wrapper as the one in the OP, but after playing with it,that one seemed to have the same issue - it's easy to get stuff out, but if you need to push data into the directive it doesn't want to go.
Interestingly, in my solution above, I can see that the `scope.options.selected' object actually contains the data, it just never gets down the the scope of the ui-select directive, and thus never allows us to push data in.
After encountering a similar issue with a different wrapper directive in a project I am working on, I figured out how to push data down through the different scopes.
My solution was to modify the ui-select script itself, adding an internal $watch function that checked for a variable in it's $parent scope. Since the ui-select directive uses scope: true, it creates a child scope (which, if I am not mistaken, the parent would be the directive in this OP).
Down at the bottom of the link function of the uiSelect directive I added the following watch function:
scope.$watch(function() {
return scope.$parent.myVar;
}, function(newVal) {
$select.selected = newVal;
})
In the link function of our directve here, I added this $watch function:
scope.$watch(function() {
return ngModel.$viewValue;
}, function(newVal) {
scope.myVar = newVal;
})
So what happens here is that if the $viewValue changes (i.e., we assign some data from a http service, etc. to the dataModel binding, the $watch function will catch it and assign it to scope.myVar. The $watch function inside the ui-select script watches scope.$parent.myVar for changes (We are telling it to watch a variable on the scope of it's parent). If it sees any changes it pushes them to $select.selected - THIS is where ui-select keeps whatever values that have been selected by clicking an item in the dropdown. We simply override that and insert whatever values we want.
Plunker - Two-way binding
First of all dataModel is a string. Since you defined multiple the model would be an array.
What's more important is that the uiSelectdirective creates a new scope. That means that ng-model="ngModel" does no longer point to dataModel. You effectively destroy the binding.
In your controller make dataModel an object:
$scope.dataModel = {};
In your directive let the selected values be bound to a property:
return '<ui-select multiple ng-model="ngModel.selection"
Now the the selected values will be bound to dataModel.selection.
If you don't use the ngModelController you shouldn't use ng-model with your directive.
I have a select like this:
<select ng-model="myType"
ng-options="type in types | filter:compatibleTypes">
compatibleTypes is defined as a function in $scope by controller:
$scope.compatibleTypes = function(item) {
//Returns true or false
//depending on another model
}
In the controller I need to know the first filtered element (if any) of array 'type'.
Or more precisely, I have to set the value of the model 'myType' with the first element of the filtered list, when it changes.
filter is an Angular native filter. That means it can be injected to many places, including your controller:
var myController = ['$scope', 'filterFilter', function ($scope, filterFilter) {
$scope.compatibleTypes = function(item) {
//Returns true or false
//depending on another model
}
$scope.filteredResults = filterFilter($scope.types, $scope.compatibleTypes);
}];
Inject $filter to your controller
function myCtrl($scope, $filter)
{
}
Then you can just use the filter in your controller and you don't have to use it in your HTML anymore!
$scope.filteredTypes = $filter('filter')(types, $scope.compatibleTypes);
In your HTML you can use ng-options="type in filteredTypes"
And in your controller
$scope.filteredTypes[0]
to get the first one!