We're using a tree-style navigation element which needs to allow other directives/controllers to know:
What the current selection is, and
When the row selection changes
I'm trying to determine the best angular-way to handle this.
Until now, we've been firing an event the entire app can listen to - less than ideal but ensures nothing is hard-coded to communicate directly with the component.
However, we now have a need to obtain the current selection when another component is activated. An event won't fly.
So I'm considering a service, some singleton which holds the current selection and can be updated directly by the tree, and read from by anyone who needs it.
However, this present some other issues:
Would it be better to ditch the event entirely, and have components which need to know when it changes $watch the service's nodeId?
If I use $watch, it seems like I should expose the object directly - so using getters/setters won't work unless I want to complicate the needed $watch code?
Part of my concern is that this would allow any component to set the value and this is intentionally not something we'll allow - the change will not impact the tree but will de-sync the service values from the true values, and will fire invalid $watches.
Implementing a getter should not lead to a complicated $watcher:
Service:
angular.service('myService', function() {
var privateVar = 'private';
return {
getter: function() {
return privateVar;
};
});
Controller:
angular.controller('myController', function(myService){
$scope.watch(myService.getter, function(){
//do stuff
};
});
See this plunker: http://plnkr.co/edit/kLDwFg9BtbkdfoSeE7qa?p=preview
I think using a service should work and you don't need any watchers for it.
In my demo below or in this fiddle I've added the following:
One service/factory sharedData that's storing the data - selection and items
Another service for eventing sharedDataEvents with observer/listener pattern.
To display the value in component2 I've used one-way binding, so that component can't change the selection.
Also separating data from events prevents a component from changing the selection. So only MainController and Component1 can change the selection.
If you're opening the browser console you can see the listeners in action. Only listener of component3 is doing something (after 3 selection changes it will do an alert) the others are just logging the new selection to console.
angular.module('demoApp', [])
.controller('MainController', MainController)
.directive('component1', Component1)
.directive('component2', Component2)
.directive('component3', Component3)
.factory('sharedData', SharedData)
.factory('sharedDataEvents', SharedDataEvents);
function MainController(sharedData) {
sharedData.setItems([{
id: 0,
test: 'hello 0'
}, {
id: 1,
test: 'hello 1'
}, {
id: 2,
test: 'hello 2'
}]);
this.items = sharedData.getItems();
this.selection = this.items[0];
}
function Component1() {
return {
restrict: 'E',
scope: {},
bindToController: {
selection: '='
},
template: 'Comp1 selection: {{comp1Ctrl.selection}}'+
'<ul><li ng-repeat="item in comp1Ctrl.items" ng-click="comp1Ctrl.select(item)">{{item}}</li></ul>',
controller: function($scope, sharedData, sharedDataEvents) {
this.items = sharedData.getItems();
this.select = function(item) {
//console.log(item);
this.selection = item
sharedData.setSelection(item);
};
sharedDataEvents.addListener('onSelect', function(selected) {
console.log('selection changed comp. 1 listener callback', selected);
});
},
controllerAs: 'comp1Ctrl'
};
}
function Component2() {
return {
restrict: 'E',
scope: {},
bindToController: {
selection: '#'
},
template: 'Comp2 selection: {{comp2Ctrl.selection}}',
controller: function(sharedDataEvents) {
sharedDataEvents.addListener('onSelect', function(selected) {
console.log('selection changed comp. 2 listener callback', selected);
});
},
controllerAs: 'comp2Ctrl'
};
}
function Component3() {
//only listening and alert on every third change
return {
restrict: 'E',
controller: function($window, sharedDataEvents) {
var count = 0;
sharedDataEvents.addListener('onSelect', function(selected, old) {
console.log('selection changed comp. 3 listener callback', selected, old);
if (++count === 3) {
count = 0;
$window.alert('changed selection 3 times!!! Detected by Component 3');
}
});
}
}
}
function SharedData(sharedDataEvents) {
return {
selection: {},
items: [],
setItems: function(items) {
this.items = items
},
setSelection: function(item) {
this.selection = item;
sharedDataEvents.onSelectionChange(item);
},
getItems: function() {
return this.items;
}
};
}
function SharedDataEvents() {
return {
changeListeners: {
onSelect: []
},
addListener: function(type, cb) {
this.changeListeners[type].push({ cb: cb });
},
onSelectionChange: function(selection) {
console.log(selection);
var changeEvents = this.changeListeners['onSelect'];
console.log(changeEvents);
if ( ! changeEvents.length ) return;
angular.forEach(changeEvents, function(cbObj) {
console.log(typeof cbObj.cb);
if (typeof cbObj.cb == 'function') {
// callback is a function
if ( selection !== cbObj.previous ) { // only trigger if changed
cbObj.cb.call(null, selection, cbObj.previous);
cbObj.previous = selection; // new to old for next run
}
}
});
}
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js"></script>
<div ng-app="demoApp" ng-controller="MainController as ctrl">
<p>Click on a list item to change selection:</p>
<component1 selection="ctrl.selection"></component1> <!-- can change the selection -->
<component2 selection="{{ctrl.selection}}"></component2>
<component3></component3>
</div>
Related
We're using Angular-ui-router for our app state management.
A problem we're having is that every component refreshes when the $state changes, even if it's a variable that is updated that does not exist/not used in some components.
For example, we have a TickersPanel and a TagsPanel. When a Ticker is selected, the TagsPanel should be updated and refreshed, however when a tag is selected in the TagsPanel, the TickersPanel should NOT refresh, but it currently does.
I found out that { notify: false } is another object that can be passed in. However once I do that, NO components will refresh.
const save = (tag, terms) => {
CONTAINER.push(tag);
$state.go('charting', Term.write(terms), { notify: false }).then((stateResult) => {
console.log('stateResult', stateResult);
});
};
Has anyone found a way around this problem?
TagsPanel $onInit (Runs on every $state change)
this.$onInit = () => {
tagsCol.addEventListener('scroll', scrollingTags);
this.tags = [];
this.limit = 30;
const panelOpen = Dashboard.panelStatus(this.tagsOpen, this.chartMax);
panelOpen ? buildTagList() : collapsePanel();
};
TickersPanel $onInit (Runs on every $state change, if $state.params.ticker did not change, then this should not be ran)
this.$onInit = () => {
this.tickers = [];
this.spy = {
longname: "SPDR S&P 500",
ticker: "SPY",
};
this.showTickersPanel = Dashboard.panelStatus(this.tagsOpen, this.chartMax);
// const currentState = $state.params;
// const prevState = Dashboard.getPreviousState();
loadTickers().then(() => this.loadingTickersDone = true);
};
I've made a JsFiddle to describe a way to achieve what I've understood from your question using the uiRouter.
The fiddle uses state inheritance and named views to only reinitialize the tags state when the tickers param changes.
[..]
// Using # on the view names to refer to the absolute uiViews.
$stateProvider.state('tickers', {
url: "",
views: {
'tickersPanel#': {
template: [..]
bindToController: true,
controllerAs: "$ctrl",
controller: function() {
this.$onInit = function() {
console.info('Tickers Panel $onInit');
this.tickers = ["Ticker1", "Ticker2", "Ticker3"]
}
}
},
[..]
}
});
$stateProvider.state('tags', {
parent: 'tickers',
url: "/:ticker",
views: {
'tagsPanel#': {
template: [..]
controllerAs: '$ctrl',
bindToController: true,
controller: function($stateParams) {
this.$onInit = function() {
console.info('Tags Panel 2 $onInit');
this.ticker = $stateParams["ticker"];
}
}
}
}
});
[..]
Why not just maintain the state in your component and return at the beginning of the function if the state hasn't changed?
Below is my plnkr
http://plnkr.co/edit/f6LYS2aGrTXGkZ3vrIDD?p=preview
I have issue on Search Page
angular.module('plexusSelect', []).directive('plexusSelect', ['$ionicModal',
function($ionicModal) {
// Runs during compile
return {
scope: {
'items': '=',
'text': '#',
'textIcon': '#',
'headerText': '#',
'textField': '#',
'textField2': '#',
'valueField': '#',
'callback': '&'
},
require: 'ngModel',
restrict: 'E',
templateUrl: 'templates/plexusSelect.html',
link: function($scope, iElm, iAttrs, ngModel) {
if (!ngModel) return; // do nothing if no ng-model
$scope.allowEmpty = iAttrs.allowEmpty === 'false' ? false : true;
$scope.defaultText = $scope.text || '';
$ionicModal.fromTemplateUrl('plexusSelectItems.html', {
'scope': $scope
}).then(function(modal) {
$scope.modal = modal;
$scope.modal['backdropClickToClose'] = false;
});
$scope.showItems = function($event) {
$event.preventDefault();
$scope.modal.show();
};
$scope.hideItems = function() {
$scope.modal.hide();
};
/* Destroy modal */
$scope.$on('$destroy', function() {
$scope.modal.remove();
});
$scope.viewModel = {};
$scope.clearSearch = function() {
$scope.viewModel.search = '';
};
/* Get field name and evaluate */
$scope.getItemName = function(field, item) {
return $scope.$eval(field, item);
};
$scope.validateSingle = function(item) {
$scope.text = $scope.$eval($scope.textField, item) + ($scope.textField2 !== undefined ? " (" + $scope.$eval($scope.textField2, item) + ")" : "");
$scope.value = $scope.$eval($scope.valueField, item);
$scope.hideItems();
if (typeof $scope.callback == 'function') {
$scope.callback($scope.value);
}
ngModel.$setViewValue($scope.value);
};
$scope.$watch('text', function(value) {
if ($scope.defaultText === value) $scope.placeholder = 'placeholderGray';
else $scope.placeholder = 'placeholderBlack';
});
}
};
}
])
Where in I have reference http://code.ionicframework.com/1.0.0-beta.14/js/ionic.bundle.js ionic bundle than my second directive search filter will stop working but at the same time, if I reference http://code.ionicframework.com/1.0.0-beta.1/js/ionic.bundle.js it works search filter in both directive.
In beta.14 angularjs 1.3 is used and in beta.1 angularjs 1.2
So somebody told me that it can be the migration issue, But I check angularjs migration documentation but I could not find anything. Kindly somebody help me what can be the issue.
Problem:
This is due to the following breaking change in Angular 1.3.6.
Excerpt:
filterFilter: due to a75537d4,
Named properties in the expression object will only match against
properties on the same level. Previously, named string properties
would match against properties on the same level or deeper.
...
In order to match deeper nested properties, you have to either match
the depth level of the property or use the special $ key (which still
matches properties on the same level or deeper)
In the first use of your directive items have the following structure:
[
{ property: 'Value' }
]
And in your second use:
[
{ Destination: { property: 'Value' } }
]
Sadly a bug fix that you probably need wasn't introduced until 1.3.8:
filterFilter:
make $ match properties on deeper levels as well
(bd28c74c, #10401)
let expression object {$: '...'} also match
primitive items (fb2c5858, #10428)
Solution:
Use Ionic with AngularJS 1.3.8 or later.
Change your HTML to the following:
<label ng-repeat="item in items | filter: { $: viewModel.search }" ...
Initialize viewModel.search as an empty string:
$scope.viewModel = { search: '' };
Demo: http://plnkr.co/edit/ZAM33j82gT4Y6hqJLqAl?p=preview
Here is my directive
angular.module('categoryListingDirective', []).directive('categoryListing', function (Category) {
return {
restrict: 'A',
replace: true,
require: true,
scope: true,
templateUrl: '../static/partials/categoryListingForm.html',
link: function (scope, element, attrs) {
var categories = undefined;
var getCategories = function () {
if (categories === undefined) {
categories = Category.query(function(){});
}
return categories;
};
var allParentCategories = function () {
console.log('getting parent categories');
return _.uniq(getCategories(), function (category) {
console.log('category:', category);
return category.parent;
});
};
console.log('categories:', getCategories());
console.log('allParentCategories:', allParentCategories());
}
}
});
When I try to run this on my browser, I see the following in console log
categories:
[$promise: Object, $resolved: false]
categoryListing.js:25
getting parent categories categoryListing.js:19
allParentCategories: []
I am pretty sure this should not be empty.
Question
Is it because of async nature of calls getting fired?
How do I fix it, what are the recommendations?
Thanks
UPDATE
The Category resource looks like
angular.module('categoryService', ['ngResource']).factory('Category', function($resource) {
return $resource('categories/:categoryId', {categoryId: '#uuid'});
});
(Marked as community wiki since the answer is really in the comments)
This might do the trick and help you to understand the way to handle promises returned by $resource:
var allParentCategories = function () {
console.log('getting parent categories');
var ret = [];
getCategories.then(function(results){
ret = _.uniq(results.data, function (category) {
console.log('category:', category);
return category.parent;
});
});
return ret;
};
I know that it's less pretty than just directly returning the results from _.uniq, but that is life in the current state of JS!
I want to watch angular factory variable from inside directive, and act upon change.
I must be missing something fundamental from Javascript, but can someone explain, why approach (1) using inline object works, and approach (2) using prototyping does not?
Does prototype somehow hide user variable scope from angular $watch?
How can i make this code more clean?
(1):
Plunkr demo
angular.module('testApp', [
])
.factory('myUser', [function () {
var userService = {};
var user = {id : Date.now()};
userService.get = function() {
return user;
};
userService.set = function(newUser) {
user = newUser;
};
return userService;
}])
.directive('userId',['myUser',function(myUser) {
return {
restrict : 'A',
link: function(scope, elm, attrs) {
scope.$watch(myUser.get, function(newUser) {
if(newUser) {
elm.text(newUser.id);
}
});
}
};
}])
.controller('ChangeCtrl', ['myUser', '$scope',function(myUser, $scope) {
$scope.change = function() {
myUser.set({id: Date.now()});
};
}]);
(2):
Plunkr demo
angular.module('testApp', [
])
.factory('myUser', [function () {
var user = {id : Date.now()};
var UserService = function(initial) {
this.user = initial;
}
UserService.prototype.get = function() {
return this.user;
};
UserService.prototype.set = function(newUser) {
this.user = newUser;
};
return new UserService(user);
}])
.directive('userId',['myUser',function(myUser) {
return {
restrict : 'A',
link: function(scope, elm, attrs) {
scope.$watch(myUser.get, function(newUser) {
//this watch does not fire
if(newUser) {
elm.text(newUser.id);
}
});
}
};
}])
.controller('ChangeCtrl', ['myUser', '$scope',function(myUser, $scope) {
$scope.change = function() {
myUser.set({id: Date.now()});
};
}]);
Case 1:
myUser.get function reference is called without a context (return user), and the user object is returned as a closure variable.
Case 2:
myUser.get function reference is called without a context (return this.user), and so this.user only just don't throw an error because you are not in "strict mode" where this is pointing to the window object, thus resulting in this.user being just undefined.
What you actually missed is the fact that giving myUser.get as a watcher check function is giving a reference to a function, which will not be applied to myUser as a context when used by the watcher.
As i remember angular watches only properties belonging to the object.
The watch function does this by checking the property with hasOwnProperty
Currently, I am writing a test (using Jasmine) for a directive, and I suspect the link function is not being triggered.
The directive is as follows:
.directive('userWrapperUsername', [
'stringEntryGenerateTemplate',
'stringEntryGenerateLinkFn',
// UserWrapper username column
// Attribute: 'user-wrapper-username'
// Attribute argument: A UserWrapper object with a 'newData' key into an
// object, which contains a 'username' key holding the
// UserWrapper's username
function(stringEntryGenerateTemplate, stringEntryGenerateLinkFn) {
return {
template: stringEntryGenerateTemplate('username'),
restrict: 'A',
scope: true,
link: stringEntryGenerateLinkFn('userWrapperUsername', 'username')
};
}
])
So it makes use of 2 functions provided through factories, namely stringEntryGenerateTemplate and stringEntryGenerateLinkFn.
The stringEntryGenerateTemplate function takes in a string and returns a string.
The stringEntryGenerateLinkFn function, when called returns the actual link function. It mostly consists of event handlers so I shall simplify it to:
function stringEntryGenerateLinkFn(directiveName, key) {
return function(scope, element, attr) {
scope.state = {};
scope.userWrapper = scope.$eval(attr[directiveName]);
}
}
Here is how I use the directive:
<div user-wrapper-username="u"></div>
Here is my test case:
describe('UserWrapper Table string entry', function() {
var $scope
, $compile;
beforeEach(inject(function($rootScope, _$compile_) {
$scope = $rootScope.$new();
$compile = _$compile_;
}));
it('should be in stateDisplay if the value is non empty', function() {
var userWrapper = {
orgData: {
student: {
hasKey: true,
value: 'abcdef'
}
},
newData: {
student: {
hasKey: true,
value: 'abcdef',
changed: false
}
}
}
, key = 'student'
, elem
, elemScope;
$scope.userWrapper = userWrapper;
elem = $compile('<div user-wrapper-username="userWrapper"></div>')($scope);
elemScope = elem.scope();
expect(elemScope.userWrapper).toBe(userWrapper);
expect(elemScope.state).toEqual(jasmine.any(Object)); // this fails
});
});
So I get a test failure saying that elemScope.state is undefined. Recall that I had a scope.state = {}; statement and it should be executed if the link function is executed. I tried a console.log inside the link function and it is not executed as well.
So how do I trigger the link function?
Thanks!
It turns out that I have to initialize the module containing the factories stringEntryGenerateTemplate and stringEntryGenerateLinkFn, which is the same module that contains the userWrapperUsername directive by adding this into my test case:
beforeEach(module('userWrapper', function() {}));
where userWrapper is the name of the module.
So the test case becomes:
describe('UserWrapper Table string entry', function() {
var $scope
, $compile;
beforeEach(module('userWrapper', function() {}));
beforeEach(inject(function($rootScope, _$compile_) {
$scope = $rootScope.$new();
$compile = _$compile_;
}));
it('should be in stateDisplay if the value is non empty', function() {
var userWrapper = {
orgData: {
student: {
hasKey: true,
value: 'abcdef'
}
},
newData: {
student: {
hasKey: true,
value: 'abcdef',
changed: false
}
}
}
, key = 'student'
, elem
, elemScope;
$scope.userWrapper = userWrapper;
elem = $compile('<div user-wrapper-username="userWrapper"></div>')($scope);
elemScope = elem.scope();
expect(elemScope.userWrapper).toBe(userWrapper);
expect(elemScope.state).toEqual(jasmine.any(Object)); // this fails
});
});
This seems like quite a big oversight on my part. Hopefully this will help anyone facing a similar issue.