I am picking up Angular for a project of mine and am having trouble getting my first steps right.
Specifically, I can get a list of items to display via a component and appropriate template, but I can not figure out how to trigger ng-click events using the component model. Many similar problems to this have been answered on SO but I have followed the many corrections and suggestions without progress and need some advice.
file: customerList.js
function CustomerListController($scope, $element, $attrs, $http) {
this.customerList = [
{ name: 'Arya' },
{ name: 'No One' },
];
this.yell = function(customer) {
console.log("customer customer, we've got a click");
};
}
angular.module('myApp').component('customerList', {
templateUrl: 'customerList.html',
controller: CustomerListController,
});
And its template:
file: customerList.html
<div class="customer"
ng-repeat="customer in $ctrl.customerList"
customer="customer"
ng-click="$ctrl.yell(customer);">
Welcome home, {{customer.name}}!
</div>
Even when I set ng-click="console.log('click detected');", I get no console log.
I believe this is sufficient information to diagnose but please let me know if you need more.
Thanks!
First of all, console.log will not work directly in an angular expression. You can't use window functions directly in expressions.
Second, I would recommend using controllerAs syntax as it's a newer school way of doing things. Try accessing the controller with your controllerAs alias in the ng-click() expression.
Related
I'm trying to figure out how to get data into a component transclusion in Angular 1.6.4. The scenario has a component, a directive (not re-written as a component yet) and a service for inter-component communication.
angular.module('app')
.service('svc', function() {
this.connector = {};
})
.directive('first', ['svc', function($svc) { return {
restrict: 'E',
scope: { 'id': '#' },
template: '<button ng-click="GetData()">get data</button>',
controller: ['$scope', 'svc', function($scope, $svc) {
$scope.connector = { data: [] };
$svc.connector[$scope.id] = $scope.connector;
$scope.GetData = function() {
// This is a mock-up; I'm really doing a REST call.
$scope.connector.data = [
{id: 0, name: 'one'},
{id: 1, name: 'two'}
];
};
}]
}; }])
.component('second', {
bindings: { parent: '#firstid' },
transclude: true,
template: '<ng-transclude></ng-transclude>',
controller: ['svc', function($svc) {
this.data = $svc.connector[this.parent];
// Not sure what to do here
}]
})
;
My HTML looks something like this:
<first id="first-thing"></first>
<second firstid="first-thing">
Where I expect my data to be: {{$ctrl | json}}<br/>
... but maybe here: {{$ctrl.$parent | json}}<br/>
... or even here: {{$parent | json}}<br/>
<div ng-repeat="item in $ctrl.data">
<p>Output: {{item.id}}/{{item.name}}</p>
</div>
</second>
These may not be nested with a require, which is why I'm using a service to store my data; <first><second></second></first> is not an option. I can manage getting data from the service inside the component controller using some $onInit workarounds where necessary. I've checked and the service contains the correct data at the correct times. In the interest of component reuse, I need the controller to transclude content.
Batarang lists all my scopes. The directive has a scope, $id 6 (there are other things on the page), as expected. The component has a scope, $id 7, as expected. These scopes contain the correct data based on what I've put in them and what I'd expect.
My problem is that I have an additional scope, $id 8. It appears to be the transcluded scope and it is a sibling of 6 and 7 (these are peers on $id 5, my page controller). As noted in my HTML snark, I expected the component transclusion to live in 7. I would be fine if 8 was a child scope of 7, but it's a disconnected sibling. I tried additional bindings but I can't get them to populate so they just throw. I'm clearly doing something wrong because what I'm getting is the model that pre-1.3 used for transclusion scope inheritance.
Can someone tell me where I've gone astray or at least point me towards the correct solution?
I've figured it out. In passing, I should note that according to the literature on the Internet, I'm doing something that I probably shouldn't do. I understand where the authors of Angular are coming from with trying to isolate scopes down the chain but I don't agree with that model, at least for transclusion.
angular.module('app')
.service('svc', function() {
this.connector = {};
})
.directive('first', ['svc', function($svc) { return {
restrict: 'E',
scope: { 'id': '#' },
template: '<button ng-click="GetData()">get data</button>',
controller: ['$scope', 'svc', function($scope, $svc) {
$scope.connector = { data: [] };
$svc.connector[$scope.id] = $scope.connector;
$scope.GetData = function() {
// This is a mock-up; I'm really doing a REST call.
$scope.connector.data = [
{id: 0, name: 'one'},
{id: 1, name: 'two'}
];
$scope.connector.data.Update($scope.connector.data);
};
}]
}; }])
.component('second', {
bindings: { parent: '#firstid' },
transclude: true,
template: '<ng-transclude></ng-transclude>',
controller: ['$element', '$transclude', '$compile', 'svc', function($element, $transclude, $compile, $svc) {
this.$onInit = () => { angular.extend(this, $svc.connector[this.parent]; };
var parentid = $element.attr('firstid');
$transclude((clone, scope) => {
$svc.connector[parentid].Update = (data) => {
angular.extend(scope, data);
$element.append($compile(clone)(scope));
};
});
}]
})
;
How it works
This is essentially manual transclusion. There are too many examples on the Internet about manual transclusion where people modify the DOM manually. I don't completely understand why some people think this is a good idea. We jump through so many hoops to separate our markup (HTML) from our formatting (CSS) from our code (Angular directives/components) from our business logic (Angular services/factories/providers), so I'm not going to go back to putting markup inside my code.
I found this article and a comment on an Angular issue by Gustavo Henke that used the scope inside $transclude to register a callback. With that key bit of information, I figured I could do much more scope manipulation.
The code in $transclude seems to be outside the digest cycle. This means that anything touched inside it will not receive automatic updates. Luckily, I have control of my data's change events so I pushed through this callback. On the callback, the data are changed and the element is recompiled. The key to locate the callback in the service hasn't been bound from the controller tag yet so it has to be retrieved from the attributes manually.
Why this is bad
Components are not supposed to modify data outside their own scope. I am specifically doing exactly not-that. Angular doesn't seem to have a more appropriate primitive for doing this without breaking some other concern that's more important to leave intact, in my mind.
I think there's a, "memory leak," in this, which is to say that my element and scope aren't being disposed of correctly with each update cycle. Mine uses fairly little data, it is updated only directly by the user with a throttle and it's on an administration interface; I'm okay with leaking a little memory and I don't expect the user will stay on the page long enough for it to make a difference.
My code all expects things to be in the right place and named the right things in the markup. My real code has about four times as many lines as this and I'm checking for errors or omissions. This is not the Angular way which means I'm probably doing something wrong.
Credits
Without the Telerik article, I would have been sitting next to an even bloodier mark on my wall right now.
Thanks to Ben Lesh for his comprehensive post about $compile with appropriate disclaimers about how one shouldn't use it.
Todd Motto helped a bunch with how to write a decent Angular 1.x component in his post on upgrading to 1.6. As one may expect, the Angular documentation on components doesn't offer much more than specific pointers to exactly what things are called.
There's a little information at the bottom of AngularJS issue 7842 that does something similar and may even have a better method for managing scoped data more appropriately than I did.
Building a small search app using Elasticsearch and AngularJS. 2 pages, home and results page. Everything is working except... I have this custom search directive that I'm trying to pass the value of a service into. The service is a variable that is bound to ngModel in my controller.
How can I pass the value of searchTerms from home to the results page?
My service
.service('queryService', function() {
var searchTerms;
this.searchTerms = null;
I pass my service into the controller and set it to $scope
$scope.searchTerms = queryService.searchTerms;
I then $watch it for changes
$scope.$watch('searchTerms', function() {
queryService.searchTerms = $scope.searchTerms;
});
My directive looks like this
.directive('searchResults', ['queryService', function(queryService) {
return {
restrict: 'E',
replace: true,
//priority: 1001,
scope: {
searchTerms: "=",//ngModel
results: "=",
websiteUrls: "=",
suggestions: "&",
search: "&"
},
templateUrl: 'search/search-results.html',
link: function(scope, element, attrs) {
}
}
}]);
My search input:
<input type="text" name="q" ng-model="searchTerms" placeholder="Search" class="form-control input-lg" id="search-input" uib-typeahead="query for query in getSuggestions($viewValue)" typeahead-on-select="search($item)" autofocus>
I have 2 way data binding working, but no autocomplete(Angular UI Bootstrap Typeahead) or search functionality. I'm pretty sure something is suppose to go in the link function, just not sure what... still learning AngularJS directives.
NOTE: Everything works if I perform a search from the results page.
More Info So basically what I'm trying to do is a user enters a search Term on the home page. searchTerms is my ngModel. I'm using AngularJS UI Bootstrap Typeahead for autocomplete functionality(as can be seen on the input tag). I have a queryService that initiates searchTerms to null and the queryService is DI into the controller. I have a directive that has isolate scope (scope: {}), where I am passing searchTerms, the results object and both the autocomplete and search functions. I'm using ngRoute for now because I'm trying to keep this simple until I have it working - its only 2 pages.
HTML Snippet
<search-results ng-model="searchTerms" website-urls="page" results="results" uib-typeahead="query for query in getSuggestions($viewValue)" typeahead-on-select="search($item)"></search-results>
Light Bulb moment, maybe
As I continue to learn more about directives, I think I just solved the problem. I had all this working initially without the use of a directive. Using routes, templates and controllers. I should just be able to use my current controller in my directive, right?
The only reason why I want to use a directive is because it seems to be the best option when AngularJS is used in combination with a CMS.
Am I on the right track now?
After further reading (and understanding), it seems that I can use the controller I already have in my directive and that should solve this problem quite nicely. I will post results after completing and testing it.
I've inherited another legacy system that a previous colleague was working on, he was asked to remove server side components from an app and replace them with AngularJS... it's an old JSP system and everything is a bit of a hack. Anyway, there is a jQuery library within the old system that produces Modal Windows, rather than have to add UI-Bootstrap my colleague converted this jQuery service to an AngularJS directive and it kind of works... but with one exception, here is the directive code...
.directive('popupWindow', ['$document', '$compile', function ($document, $compile, MessagesService) {
"use strict";
return {
restrict: 'A',
scope: {},
link: function (scope, element, attrs) {
var newPopup = new Basebox({
contentUrl: '/ng/xtemplate/widgets/CCC/' + scope.getType + '.html',
type: 'regular',
id: 'sf-basebox',
hideBackground: false,
overlay: true,
resolveTest: {getType: scope.getType, message: scope.message, config: scope.config }, // this is appended to the Modal Window as a ng-init and it renders correctly!
closeOnOverlay: false,
closeIcon: true,
checkOldIe: true,
afterContentLoad: function () {
$compile($document.find("#basebox"))(scope);
scope.$apply();
}
});
newPopup.create();
}
});
}
};
}]);
Now this works as it allows us to import HTML templates, it produces the Modal Window and allows us to set various properties, it also allows is to pass data objects (see the resolveTest property) to the controller of the model window. Everything works! Anything within the angularJS templating {{ }} is rendered and we can use angular directives such as ng-init, ng-repeat, etc, etc... however when trying to have two way binding between the view and the controller nothing seems to work, the data gets rendered from the controller to the view but not the other way around. The view never updates... here is the view and the controller
.controller('reportModelCtrl', function ($scope) {
$scope.val = {};
$scope.val.foo = 'blue';
})
Here is the view...
<div ng-controller="reportModelCtrl">
<input type="text" ng-model="val.foo"> {{ val.foo }} this is by default 'blue' but never updates
</div>
Like I say, I dynamically add an ng-init to the modal template and this works, but when I amend the text box nothing happens. Can anyone think of something I'm missing? Even if I try to update values using methods in the controller and ng-click in the view nothing is updated. Calling $scope.$apply() just results in an error. Do I need to add a form to my view? Sorry the jQuery Basebox library isn't included here but I just thought it would add to the confusion. I just really want suggestions or hints to getting the two way binding to work rather than a full answer.
I am trying to call (or use) few custom directives in ionic framework, dynamic is like <mydir-{{type}} where {{type}} will come from services and scope variable, having values radio, checkbox, select etc, and created my directives as mydirRadio, MydirCheckbox, mydirSelect, But its not working.
Is their any good approach to get the dynamic html as per {{type}} in scope?
Long story short; no you can't load directives dynamically in that way.
There are a few options for what you can do. You can, as other answers have mentioned, pass your context as an attribute (mydir type="checkbox"). You could make a directive that dynamically loads another directive, as also mentioned by others. Neither of these options are imo every good.
The first option only works if you write the directive yourself, not when using something like ionic. It also requires you to write multiple directives as one, which can get very messy very quickly. This mega directive will become hard to test and easy to mess up when maintaining it in the future. Note that this is the correct way to pass data to a directive from the view, it's just not good for this specific use case.
The second option is problematic because obfuscates things a bit too much. If someone reads your html and sees a directive called dynamic that is given dynamic data... they have no idea what is going to happen. If they see a directive called dropdown that is given a list they have a fair idea of what the result will be. Readability is important, don't skimp on it.
So I would suggest something simpler that requires much less work from you. Just use a switch:
<div ng-switch="type">
<mydir-select ng-switch-when="select"></mydir-select>
<mydir-checkbox ng-switch-when="checkbox"></mydir-checkbox>
</div>
I dont understand why do you need dynamic directives.
Simple use single directive and change the template accordingly.
For example -
angular.module('testApp')
.directive('dynamicDirective', function($compile,$templateCache,$http) {
return {
restrict: 'C',
link: function($scope,el) {
//get template
if(radio){
$http.get('radio.html', {cache: $templateCache}).success(function(html){
//do the things
el.replaceWith($compile(html)($scope));
});
} else if(checkbox){
//load checkbox template
} //vice-versa
}
};
});
You can inject service variable in directive also.
a bit more code would help. I don't know, if its possible to do dynamic directives like the ones in a tag
<{dyntag}></{dyntag}>
but you also can use an expression like
<your-tag dynamic_element="{type}">...</your-tag>
which should have exactly the same functionality. In your case it would be like:
Your JSObject ($scope.dynamics):
{"radio", "checkbox", "select"}
and your HTML:
<div ng-repeat="dyn in dynamics">
<your-tag dynamic_element="{dyn}"></your-tag>
</div>
Yes, that's not a problem. You can interpolate your data using {{}} and in your directive compile a new element using that data:
myApp.directive('dynamic', function($compile, $timeout) {
return {
restrict: "E",
scope: {
data: "#var" // say data is `my-directive`
},
template: '<div></div>',
link: function (scope, element, attr) {
var dynamicDirective = '<' + scope.data + ' var="this works!"><' + scope.data + '>';
var el = $compile(dynamicDirective)(scope);
element.parent().append( el );
}
}
});
HTML:
<div ng-controller="MyCtrl">
<dynamic var="{{test}}"></dynamic>
</div>
Fiddle
I'm using ui.grid to get a list of parts. I've created a column that contains a button which launches a modal. What I'm having trouble with is sharing the scope of the part that is contained in the row. I want to share the properties of that row with the the button that I'm creating using cellTemplate. I then want to share the $scope of the part row with the modal that it will launch.
I'm a bit stumped on how to actually do this.
So far I've tried
• Wrapping an ng-repeat around the button that I want to target. This kind of works but makes the app super slow
• Data-binding on the button via ng-class. I can't seem to target this correctly.
How can you share the $scope of an object that you're receiving via $http.get into the ui.grid with elements that you're creating with cellTemplate?
Disclaimer -- I always use controllerAs syntax, so if referencing the controller in the context of HTML is weird to you, just ignore that part and pretend like you setup the methods to be directly on the scope. I also do everything in Typescript, not Javascript, so I'm going to write the pertinent parts of the code in here. They should be easy to plug into your application.
The answer is a combination of the two answers you already have from Sunil and S.Baggy.
What you want to do is use the getExternalScopes() function and attach something to the scope of the HTML where your grid resides. The thing you handed the grid will take in the row and call your modal popup. See below for a little clarification.
Your HTML -
<div ng-controller="MyController as myController">
<div ui-grid="myController.GridObject" external-scopes="myController"></div>
</div>
By using controllerAs syntax and making the controller the reference in the external scopes, we can now gain access to everything in our controller. So we can call methods in it.. In order to do that, however, we have to use a cellTemplate, which it sounds like you already know how to do, and in that cellTemplate we have to have the following:
ng-click="getExternalScopes().methodToLaunchModal()"
Now the last part of hooking all this up is to write the methodToLaunchModal() method into the controller. For that we're borrowing the code from S.Baggy's answer. Here is a very abbreviated controller with the GridObject (the same one I referenced from the controller above):
app.controller('MainCtrl', function($scope, $modal) {
GridObject = {
... setup of all the other things
columnDefs: [{ etc, etc, }, { etc, cellTemplate: '<div ng-click="getExternalScopes().methodToLaunchModal(row.entity)">whatever</div>' }]
};
methodToLaunchModal: function(row) {
var modalInstance = $modal.open({
templateUrl: 'someTemplate',
controller: 'ModalController',
resolve: {
rowObject: function () { return row; }
}
});
};
});
At this point your modal scope will have an object named rowObject on it that will have all the properties from your row. So you should be able to call rowObject.SomeProperty to get its value.
Apologies if any of the syntax is slightly off.
I use the bootstrap $modal directive with code like this...
clickFunction: function (event, row) {
event.stopPropagation(); // prevents the current row from appearing as selected
var modalInstance = $modal.open({
templateUrl: 'views/modalcontent.tpl.html',
controller: 'ModalMessageController',
size: 'lg',
resolve: {
message: function () { return row.entity.serial_number; }
}
}
);
Then I just refer to {{message}} in the template. Of course you could pass in any other piece of data too.
You can access row and its properties on row selection or ng-click of that row using externalscopes
ng-click="getExternalScopes().onRowClick(row)"
onRowClick: function (row) {
row.entity.Property1; /// and so on for all row properties
}