My directive looks like this:
angular.module('app')
.directive('chart', function () {
return {
template: '<div class="chart"></div>',
restrict: 'E',
replace: 'true',
scope: {
data: '='
},
link: function (scope, element, attrs) {
console.log(scope);
console.log(scope.data);
}
};});
and gets passed an array from my controller in the view.
<chart data="array"></chart>
The console output looks as follows:
Scope {...}
$id: "006"
$parent: Child
$root: Scope
...
data: Array[1]
0: 10300
length: 1
__proto__: Array[0]
this: Scope
__proto__: Object
and
[]
When the scope object is displayed in the console it has an 'data' attribute with length 1 and the entry '10300', but when scope.data is printed it is just an empty array '[]'.
Why? I am very confused :)
The problem was that the array was initialized in the controller as [] and then filled through a function which was called with $watch. For some reason the two console outputs seem to print different states of the controller. Solution was to call the function once in the controller to initialize the array with values straight away.
Related
I've got a directive which I want to use in different cases, but all of them will be providing a list (array) from which it will take the information.
The problem is that every array will be / can be filtered by multiple filters (and some of them are custom filters). For example:
<select-search data-list="Ctrl.players | playersByDate:Ctrl.event.datetime | orderBy: 'name'"></select-search>
Those arrays, as I said, can have one, multiple or zero filters applied, some of them custom-made and some of them not (like orderBy). The problem is that when I do this, I get a $digest cycle error with any filters (custom-made and Angular filters aswell).
First, I've read the reasons why, but I do not understand them, because I'm using a one-way binding on the list:
scope:{
property: '#',
ref: "=",
list: "<",
change: "&?"
}
Therefore, it should only be triggering once (when it's filtered the first time), because after this no other changes are made in any of the sides.
Regarding this, I would like to know how can I filter the list/array at the moment of sending it to the directive (so, not filtering and storing it via JS in another variable) without getting any $digest error.
EDIT:
As requested, I've written the code in a snippet (JsFiddle here):
(function(){
var app = angular.module("AppMd", []);
app.controller("Control", [function(){
var vm = this;
vm.players = [
{name: 'Player 1'},
{name: 'Player 2'},
{name: 'Player 3'}
];
}]);
app.directive("selectSearch", function(){
return {
restrict: 'E',
scope:{
list: "<"
},
replace: true,
template: '<ul><li data-ng-repeat="obj in list">{{obj.name}}</li></ul>',
link: function(scope, element, attrs, controller, transclude){
return false;
}
}
});
})();
<script src="https://code.angularjs.org/1.6.4/angular.min.js"></script>
<div ng-app="AppMd" ng-controller="Control as Ctrl">
<select-search data-list="Ctrl.players | orderBy: 'name'"></select-search>
</div>
As you can see, it works, but if you look at the console, you'll see the errors it triggers.
Thank you!
I want an efficient way to factor an Angular Directive that is written to display a chart.
After reading other answers here, I found a nice way to create a directive that displays a single chart without any problem.
How do I reuse the same directive to display different charts? Each chart needs a JSON object that has settings and data in order to render.
I don't want to pollute my Angular View by typing 100-150 lines of JSON and passing it in via the directive.
Details:-
Each chart has some common key/value pairs that I can leave in the directive.
How do I infuse chart specific key & value pairs in each directive?
Eg:- Say I want one chart to have green bars and the other chart to have red lines.
Angular Directive
(function () {
'use strict';
angular
.module("analytics")
.directive("angularDirectiveAmcharts", angularDirectiveAmcharts);
function angularDirectiveAmcharts() {
var directive = {
link: link,
restrict: 'A',
replace: true,
scope: {
chartdata: '=',
type: '=',
customstyle: '#',
chartsettings: '=',
chartid: '#'
},
template: '<div id="{{ chartid }}" style="{{ customstyle }}"></div>'
};
return directive;
function link(scope, elem, attrs) {
AmCharts.makeChart(scope.chartid, {
"type": "serial",
"categoryField": "date",
"autoMarginOffset": 10,
"marginRight": 20,
"marginTop": 20,
//I've deleted lots of keys and values for the sake of brevity
"dataProvider": scope.chartdata
});
}
}
})();
View
<div class="chartarea" ng-controller="pcController as vm">
<div angular-directive-amcharts chartid="chartdiv" chartdata="vm.chart_data"></div>
</div>
I am particular about maintainability because a lot of changes are going to made after I'm done with my internship.
Parts of the given code in this answer are based on another answer
You could use a service to provide a standard configuration to all of your chart directives. In this service you can define this standard configuration once and merge it with a specific configuration each time, a directive is created. This way you only have to declare minor changes in your controller.
Nonrequired but possible config binding into directive:
<div ng-controller="myCtrl">
<my-chart></my-chart>
<my-chart config="conf"></my-chart>
</div>
Specific configuration in controller:
myapp.controller('myCtrl', function ($scope) {
$scope.conf = {
graphs: [{ type: 'column' }]
};
});
Service for default configuration (using jQuerys way to deep merge objects):
myapp.service('chartService', function () {
this.defaultConfig = {
"type": "serial",
// reduced object for readabilty
};
this.getConfig = function (mergeObj) {
return $.extend(true, {}, this.defaultConfig, mergeObj);
}
});
The data is get through another service, and added to the configuration after the merge:
var config = chartService.getConfig(scope.config || {});
config.dataProvider = dataProvider.getData();
chart = AmCharts.makeChart(element[0], config);
I've prepared a fiddle, so you can take a look into an example.
I am working on an ASP.NET MVC 4 app. This app has a controller with an action that looks like the following:
public class MyController : System.Web.Http.ApiController
{
[ResponseType(typeof(IEnumerable<MyItem>))]
public IHttpActionResult Get(string id, string filter)
{
IEnumerable<MyItem> results = MyItem.GetAll();
List<MyItem> temp = results.ToList<MyItem>();
var filtered = temp.Where(r => r.Name.Contains(filter);
return Ok(filtered);
}
}
I am calling this action using the following JavaScript, which relies on the Select2 library:
$('#mySelect').select2({
placeholder: 'Search here',
minimumInputLength: 2,
ajax: {
url: '/api/my',
dataType: 'json',
quietMillis: 150,
data: function (term, page) {
return {
id: '123',
filter: term
};
},
results: function (data, page) {
return { results: data };
}
}
});
This code successfully reaches the controller action. However, when I look at id and filter in the watch window, I see the following errors:
The name 'id' does not exist in the current context
The name 'filter' does not exist in the current context
What am I doing wrong? How do I call the MVC action from my JavaScript?
Thanks!
You're not passing actual data as the parameters, you're passing a function:
data: function (term, page) {
return {
id: '123',
filter: term
};
}
Unless something invokes that function, the result will never be evaluated. Generally one would just pass data by itself:
data: {
id: '123',
filter: term
}
If, however, in your code there's a particular reason (not shown in the example) to use a function, you'll want to evaluate that function in order for the resulting value to be set as the data:
data: (function (term, page) {
return {
id: '123',
filter: term
};
})()
However, these errors also imply a second problem, probably related to however you're trying to debug this:
The name 'id' does not exist in the current context
The name 'filter' does not exist in the current context
Even if no values were being passed, id and filter still exist in the scope of the action method. They may be null or empty strings, but the variables exist. It's not clear where you're seeing that error, but it's definitely not in the scope of the action method.
I'm not too entirely sure why my computed property isn't returning updated values.
I have a list of options that a user can click through, and the action updates a property, which is an Ember Object, for the controller. I have a computed property that loops through the object, looks for keys that have non-null values for that Ember Object property, and if it does find one, returns false, otherwise true.
Here's the stuff:
App.SimpleSearch = Ember.Object.extend({
init: function() {
this._super();
this.selectedOptions = Ember.Object.create({
"Application" : null,
"Installation" : null,
"Certification" : null,
"Recessed Mount" : null,
"Width" : null,
"Height" : null,
"Heating" : null,
"Power" : null
});
},
selectedOptions: {},
numOfOptions: 0,
allOptionsSelected: function() {
var selectedOptions = this.get('selectedOptions');
for (var option in selectedOptions) {
console.log(selectedOptions.hasOwnProperty(option));
console.log(selectedOptions[option] === null);
if (selectedOptions.hasOwnProperty(option)
&& selectedOptions[option] === null) return false;
}
return true;
}.property('selectedOptions')
});
App.SimpleSearchRoute = Ember.Route.extend({
model: function() {
return App.SimpleSearch.create({
'SimpleSearchOptions': App.SimpleSearchOptions,
'numOfOptions': App.SimpleSearchOptions.length
});
},
setupController: function(controller, model) {
controller.set('model', model);
}
});
App.SimpleSearchController = Ember.ObjectController.extend({
getProductsResult: function() {
var productsFromQuery;
return productsFromQuery;
},
setSelection: function (option, selectionValue) {
this.get('selectedOptions').set(option, selectionValue);
this.notifyPropertyChange('allOptionsSelected');
},
actions: {
registerSelection: function(option) {
console.log('registering selection');
console.log(this.get('allOptionsSelected'));
console.log(this.get('selectedOptions'));
this.setSelection(option.qname, option.value);
},
The action in the controller, registerSelection is firing just fine, but I only see the console.log from the SimpleSearch model once. Once the property is computed that first time, it isn't paid attention to after that, which means that the computed property isn't observing the changes to selectedOptions whenever this is called:
setSelection: function (option, selectionValue) {
this.get('selectedOptions').set(option, selectionValue);
this.notifyPropertyChange('allOptionsSelected');
},
Edit:
I actually solved my issue without changing anything.
I've changed the following line:
this.notifyPropertyChange('allOptionsSelected');
to:
this.get('model').notifyPropertyChange('selectedOptions');
notifyPropertyChange needs to be called within the context of the model (or the Ember Object that has observers of a specific property), and the string sent as an argument is the name of the property that was updated.
After I made that change, it worked as intended.
Ember doesn't observe objects for any change on the object, it observes a single property.
How is this affecting you? Well in this method you are watching selectedOptions, but that object itself is still the same object, you might be changing properties on it, but not the object itself. And then you are telling Ember in the scope of the controller that allOptionsSelected has changed, so it regrabs it, but it doesn't recalculate it because it's not dirty, it just changed. You'd really want to say selectedOptions has changed to get allOptionsSelected to recalculate its value. Unfortunately you're doing this in the scope of the controller, so telling the controller that property has changed doesn't matter to it.
allOptionsSelected: function() {
var selectedOptions = this.get('selectedOptions');
for (var option in selectedOptions) {
console.log(selectedOptions.hasOwnProperty(option));
console.log(selectedOptions[option] === null);
if (selectedOptions.hasOwnProperty(option)
&& selectedOptions[option] === null) return false;
}
return true;
}.property('selectedOptions')
Here's a dummy example showing what things cause it to actually update.
http://emberjs.jsbin.com/iCoRUqoB/1/edit
Honestly since you're not watching particular properties I'd probably do an array, or create a method on the object that handles adding/removing/modifying the properties so you could fire from within it's scope a property change updating all parent listeners with the changes.
Ember computed property dependent keys can be a
property key. in example: 'jobTitle'
computed property key. in example: 'companyName'
property key of an object. in example:
'salesAccount.name and salesAccount.website'
Example extracted from Ember.model definition:
...
jobTitle : DS.attr('string'),
salesAccount: belongsTo('salesAccount'),
companyName: Ember.computed('jobTitle', 'salesAccount.name', {
get: function () {
return this.get('salesAccount.name');
}
}),
companyWebsite: Ember.computed('salesAccount.website', 'companyName', {
get: function () {
return this.get('salesAccount.website');
}
})
...
I have the following test:
it('Should keep location when user rejects confirmation', inject(function ($controller, $rootScope) {
var confirmStub = sinon.stub(),
eventStub = {
preventDefault: sinon.spy()
};
miscServiceStub = function () {
this.confirm = confirmStub;
};
confirmStub.returns(false);
initializeController($controller, 'Builder', $rootScope);
$rs.$broadcast('$locationChangeStart', eventStub);
expect(confirmStub).toHaveBeenCalledOnce();
expect(confirmStub).toHaveBeenCalledWith('Are you sure you want to leave? you will loose any unsaved changes.');
expect(eventStub.stopPropagation).toHaveBeenCalledOnce();
miscServiceStub = function () {};
}));
which tests this code:
$rootScope.$on('$locationChangeStart', function (event) {
dump(arguments);
if (!$miscUtils.confirm('Are you sure you want to leave? you will loose any unsaved changes.')){
event.preventDefault();
}
});
event.$stopPropagation doesn't call the mock event, and dump(arguments) shows that it is being passed into the event right after the real event object:
Chromium 31.0.1650 (Ubuntu) DUMP: Object{
0: Object{name: '$locationChangeStart', targetScope: Scope{$id: ..., $$childTail: ..., $$childHead: ..., $$prevSibling: ..., $$nextSibling: ..., $$watchers: ..., $parent: ..., $$phase: ..., $root: ..., this: ..., $$destroyed: ..., $$asyncQueue: ..., $$postDigestQueue: ..., $$listeners: ..., $$isolateBindings: ..., activeTab: ..., routeParams: ...}, preventDefault: function () { ... }, defaultPrevented: false, currentScope: Scope{$id: ..., $$childTail: ..., $$childHead: ..., $$prevSibling: ..., $$nextSibling: ..., $$watchers: ..., $parent: ..., $$phase: ..., $root: ..., this: ..., $$destroyed: ..., $$asyncQueue: ..., $$postDigestQueue: ..., $$listeners: ..., $$isolateBindings: ..., activeTab: ..., routeParams: ...}},
1: Object{stopPropagation: spy}
}
how can I make it so the event object is the mock event and not the real event object itself? Am I approaching this the right way? I'm quite new to Angular and any comments on the code/test would be greatly appreciated.
If you need any more related code please tell me.
$scope.$broadcast returns the Event object, so you can do this:
var event = $scope.$broadcast("someEvent");
expect(event.defaultPrevented).toBeTruthy();
In this line:
$rs.$broadcast('$locationChangeStart', eventStub);
you provide an argument that will be transmittet alongside with the event, not the event itself. That's why you will get here:
$rootScope.$on('$locationChangeStart', function (event)
2 objects as arguments. The full signature for your event should be:
$rootScope.$on('$locationChangeStart', function (event, eventStub)
So: you can't test the call of stopPropagation in the way you have tried it.
If you have a look at the angular src (line 12185...) you will see, that the event is created without any possibility to mock this object. And the $scope itself is not mocked by angular-mock.
If one want to test that preventDefault is called, i would write a service that has a function that calls preventDefault. This service can easily be spyed on.