I am attempting to build what should be a very simple custom directive using some hierarchical data. Each page in the list has subpages and the data is of the form:
{"Title 1": "Page Title", "Id": "1", "Pages": {"Title 1.1": "Page Title 1.1", "Id": "2"}, {"Title 1.2": "Page Title 1.2", "Id": "3"}}
and so on. The data here is just a quick illustration - there is no issue with either the data or retrieval method.
I have a directive controller set up as:
app.directive('pageSelectList', function () {
return {
restrict: 'EA',
replace: true,
scope: {
pageList: '='
},
templateUrl: '/Scripts/App/Directives/Templates/PageSelectList.html',
link: function (scope, elem, attrs) { }
};
});
The template is:
<ul class="page-list page-root">
<li ng-repeat="p in pageList.Pages">{{ p.Title }}</li>
</ul>
The directive is used with data drawn from the parent scope ($scope.listOfPages).
However, when using it, nothing is displayed - it's blank. Strangely, when replacing the directive template with the following markup:
<p>{{ pageList.Title }}</p>
The expected markup <p>Title 1</p> is shown.
It would appear that the directive has some sort of issue with either ng-repeat or that the data being used in the repeat is a subobject of the pageList object passed.
Furthermore, when the markup for the directive is just used in a regular page with data from the parent scope, there is no problem at all. It seems to be a problem with the directive itself.
EDIT
This is the page controller that is populating the data for the directive:
var pageEditController = function ($scope, $rootScope, $state, $stateParams, pageService) {
$scope.page = {};
$scope.errorMessage = "";
getPage(); // This is for other data on the page and not directly linked to directive issue.
getPages(); // This is to get the directive data
function getPage() { // Just an http get method for $scope.page }}
function getPages() {
pageService.getTree()
.success(function (result) {
$scope.pageList = result.Result; // This is where the directive data is loaded into scope
})
.error(function (error) {
$scope.errorMessage = 'Unable to load pages';
});
};
});
Further strange behaviour:
A template with this:
<p>{{ pageList.Title }}</p>
shows the Title OK.
This:
<p>{{ pageList.Title }}</p><p>{{ pageList.Id }}</p>
shows nothing at all. Obviously ng-repeat in original example doesn't work either.
In the directive, you have mentioned as "pageList". But in the template, you are accessing it using "PageList". So, I guess that it may solve using issue.
As we detect in comment: your code work OK, and problem with template for directive.
When you use replace:true in your directive, template that you load must have exactly one root element otherwise you get next error:
Error: [$compile:tplrt] Template for directive 'pageSelectList' must have exactly one root element. PageSelectList.html
https://docs.angularjs.org/error/$compile/tplrt?p0=pageSelectList&p1=PageSelectList.html
So, for solving you have two way:
1) wrap your template in container, like a div
<div>
your directive
<p>{{ pageList.Title }}</p><p>{{ pageList.Id }}</p>
</div>
2) remove flag replace, or use it replace: false.
Related
In AngularJS, when something change in parent, the child binds the new change,
automatically. However this awesome process is being done only in the HTML file,
but not in the JS file.
Sometimes, if not always, we would like to acheive the same thing also in JS files.
Directive is getting initialized via the JS file, like this:
// statusBadge.js directive
app.directive("statusBadge", ($rootScope, ArrayManipulation, Http, Cast, Modules, Toast) => {
return {
templateUrl: "/shared/status-badge/status-badge.html",
scope: {
bindModel: "=ngModel",
statusChangingRequiredModules: "=",
statusOptionsToBeHide: "<",
onChange: "=",
resource: "#"
},
link: function($scope, element, attrs) {
if ($scope.bindModel==$rootScope._id) {
$scope.active = true;
}
}
}
});
// statusBadge.html directive
<span ng-show="active">
Active
</span>
// parent.js
$scope._id = "123";
// app.js
$rootScope._id = "123";
// parent.html
<status-badge ng-model="_id"></status-badge>
Now when data is synchronomous the "Active" will be displayed, since its getting
provided by initialization.
However, when working async - like receiving the data from the remote server,
I have to include ng if, like that:
// app.js
setTimeout(function () {
$scope._id = "123";
}, 10);
// parent.html
// notice the ng-if
<status-badge ng-model="_id" ng-if="_id"></status-badge>
If I dont provide with the above ng-if, the directive configuation will be inited
without the actual data.
Front end, later, will have the data, since as I mentined in the begining - AngularJS
binds two way data, but only to HTML files.
Now I know I can put a watcher in the JS file, like this :
// statusBadge.js directive
app.directive("statusBadge", ($rootScope, ArrayManipulation, Http, Cast, Modules, Toast) => {
return {
templateUrl: "/shared/status-badge/status-badge.html",
scope: {
bindModel: "=ngModel",
statusChangingRequiredModules: "=",
statusOptionsToBeHide: "<",
onChange: "=",
resource: "#"
},
link: function($scope, element, attrs) {
$scope.$watch("bindModel", function() {
if ($scope.bindModel==$rootScope._id) {
$scope.active = true;
}
}, true);
}
}
});
However this can be tidy to so everytime, and sometimes the list of the watchers is long:
// almost all of them are two way data binding, but resource and statusOptionsToBeHide
scope: {
bindModel: "=ngModel",
statusChangingRequiredModules: "=",
statusOptionsToBeHide: "<",
onChange: "=",
resource: "#"
}
So what is the solution for this, or the best practice or just the preferred way of working?
Should I add the ng-if in each time its changed, and every time make it false - which can cause a bad UI - since the whole list, or component is going to be refreshed on each change - looks awful.
Should I put tons of watchers?
OR that is there any good solution for this?
Note that I have seen this question in Stack:
$watch'ing for data changes in an Angular directive
However they tell there that it should be a watcher in there, but I need a "multiple" watcher.
I want to call controller function from directive. Here is the fiddle. I have a sayHello() function in controller. And I want to call that function from angular directive. If i wall like scope.sayHello();
scope.sayHello is not a function
I am getting like the above in console.
To get your alert in your fiddle to fire, all I had to do what add the person into your template. You had the updateparent="updatePerson()", and you just needed to pass the person in that call, like this: updateparent="updatePerson(person)". Then your alert fired.
The reason for this is that you need to state in the template all of the parameters that you are passing in to the function. Since you call it like updateparent({person: mandatePerson}), you have to put the key person into your template that it will be called with that param. They have to match.
The Angular directive's link function has arguments for both scope and controller -- if the method you want to call is directly on $scope in your controller you can just call it off of the scope arg-- if you are using controllerAs syntax (which I would recommend as it is a recommended Angular pattern) you can call it off the controller argument.
So, for your specific case (methods directly on $scope) in your directive return object you add a property link:
link: function (scope, iElement, iAttrs, controller, transcludeFn)
scope.sayHello();
}
link runs once at directive creation-- if you want the scope or method to be available outside of that for some reason, assign it to a variable defined at the top level of the module.
I changed your directive a bit, but this is how you get that sort of functionality.
FIDDLE
If you're interested in AngularJS I would highly recommend the John papa styleguide.
https://github.com/johnpapa/angular-styleguide
It will get you using syntax like controllerAs and will help make your code cleaner.
HTML
<body ng-app="myApp" ng-controller="MainCtrl">
<div>
Original name: {{mandat.name}}
</div>
<my-directive mandat="mandat"></my-directive>
</body>
JS
var app = angular.module('myApp', []);
app.controller('MainCtrl', MainController);
function MainController($scope) {
$scope.mandat = {
name: "John",
surname: "Doe",
person: { id: 1408, firstname: "sam" }
};
}
app.directive('myDirective', MyDirective);
function MyDirective() {
return {
restrict: 'E',
template: "<div><input type='text' ng-model='mandat.person.firstname' /><button ng-click='updateparent()'>click</button></div>",
replace: true,
scope: {
mandat: "="
},
controller: function($scope) {
$scope.updateparent = function() {
$scope.mandat.name = "monkey";
}
}
}
}
I'm having a problem with a new state I added to our web site.
Short description
I have a state (/page)'q.explorer' which contains a button; when clicked it should go to a child state (named 'q.explorer.detail') but it does not. However: in the logging I see that it does try to go to that (/state) and the new url is formatted as defined in the child state.
But still the template and controller that are actually used is the 'parent' which contains the button ...
This may be a little confusing to explain; so I have also added some code in the hope that this will clarify my problem.
The setup looks like this:
$stateProvider
.state('q', {
url: '/:locale/app',
data : { ui: "V2" },
views: {
'application' : {template: '<div ui-view=""><page-home-v2></page-home-v2></div>' }
}, controller: function($scope, $stateParams, siteNavigation) {
siteNavigation.applyUrlParameters($stateParams);
}
}).state('q.explorer', {
url: '/explorer?:year&:month&:guide',
template: '<page-explorer-v2></page-explorer-v2>',
controller: function($scope, $stateParams, siteNavigation) {
console.log("controller: qlaro.explorer");
siteNavigation.applyUrlParameters($stateParams);
}
}).state('q.explorer.detail', {
url: '/detail',
template: '<page-explorer-detail-v2></page-explorer-detail-v2>',
controller: function($scope, $stateParams, siteNavigation) {
console.log("controller: qlaro.explorer.detail");
siteNavigation.applyUrlParameters($stateParams);
}
})
angular
.module('q.components')
.service('siteNavigation', function($state, $location) {
var service = this;
service.applyUrlParameters = function($stateParams) {
if (!$stateParams) $stateParams = $state.params;
console.log('Apply state parameters for state: ' + $state.current.name);
console.log('URL >> ' + $location.url());
};
};
Somewhere deep in the template of "q.explorer" there is a button to open the detail view ("q.explorer.detail"). It uses this code:
function goToDetail() {
var ui = $state.current.data.ui;
if (ui === "V1") { /*...*/ }
else if (ui === "V2") {
var params = {
year: Store.getYear(),
month: Store.getMonth(),
locale: Store.getLocale()
}
var guide = Store.getActiveSidebarItem();
if (guide) params.guide = guide.slug;
console.log("go to explorer: " + params.guide);
$state.go('q.explorer.detail', params);
}
else console.log("Unable to go to state because the ui version is not known.");
}
And this is what I see in the console after clicking the link:
go to explorer: ebit
controller: q.explorer
Apply state parameters for state: q.explorer.detail
URL >> /nl/app/explorer/detail?year=2015&month=11&guide=ebit
As you can see, it uses the controller of the 'parent' iso the child page I want to open. Even though $state.current.name is correct ... Or maybe I should say it does not change from state ...
Any help is welcome.
(PS: We are using Angular 1.4.9)
Looks like you are using nested states such that q.explorer.detail is a child of q.explorer. To render the child state's template, you also need a specific ui-view where it can be placed into. And this will be searched in the template of the parent state. Getting the console.log() output just means the controllers are instantiated, but that even happens if the template isn't rendered at all.
So check if you have an ui-view in the template of the q.explorer state. For more details, please see: https://github.com/angular-ui/ui-router/wiki/Nested-States-and-Nested-Views
You could also fix this by not making q.explorer.detail a child of q.explorer. A child state is created as soon as you need the dot notation.
Yoy have to add somewhere in 'q.explorer' state's template entry point for nested view 'q.explorer.detail', otherwise child controller will not be called.
For example:
template: '<page-explorer-v2></page-explorer-v2><ui-view></ui-view>',
instead of
template: '<page-explorer-v2></page-explorer-v2>'
See jsfiddle: http://jsfiddle.net/jcpmsuxj/42/
Upd. As #ajaegle mentioned you should to visit official docs page:
https://github.com/angular-ui/ui-router/wiki/Nested-States-and-Nested-Views
In one of my Angular.JS controllers, I have the following:
app.controller("MyController", ["$scope", function($scope){
$scope.messages = [
new Message(1),
new Message(2)
];
$scope.addMessage = function(x) { $scope.messages.push(new Message(x)); }
}]);
Then in my main HTML page, I have
<message message="message" ng-repeat="message in messages">
This is bound to a directive:
app.directive("message", function() {
return {
restrict: "E",
scope: {
message: "="
},
templateUrl: "js/Directives/message.html"
};
});
The template file is:
<li class="message">{{message.msg}} </li>
However, when I call addMessage on the controller, while it does add to $scope.messsages, it doesn't actually refresh the ng-repeat and display the new message. How can I do this?
I would suggest some structural changes in your directive.
First of all, why not refer the original array itself instead of referring value at each iteration ??
<message messages="messages">
Then you can actually move ng-repeat part in your directive template, [You must note that since you're using = in message: "=", = binds a local/directive scope property to a parent scope property. So with =, you use the parent model/scope property name as the value of the DOM attribute. ].
Hence your directive will look like :
app.directive("message", function() {
return {
restrict: "E",
scope: {
messages: "="
},
templateUrl: "js/Directives/message.html"
};
});
and the subsequent template will look something like this :
<ul>
<li ng-repeat="message in messages" class="message">{{message.msg}} </li>
</ul>
You can find a demo plunker here
I have a page with a controller that has a directive that displays a form with inputs.
The directive has an object that contains all the inputs (and their ng-model) that are displayed on the form. This binds the form inputs data to the object variable inside the directive.
I need to display results (and other actions) submiting the data of the form.
For that I created a service that handles the business logic and thae ajax calls.
The questions here is... how should I access the form data from the service to perform the required actions? I thought about accessing the directive variable from the service, but I'm not sure how to do it and if this is the right way in the first place.
The service should hold a model which is basically the javascript object of your form.
The directive should inject the service and add that object on his scope(a reference).
The directive's template should speak with the directive's scope and display the form.
Changing a value on the view will reflect the service since we they have the same reference and the view will update the directives scope since there is two way binding.
admittedly I'm still working things out, but I think if you add in a controller between your directive and service things will be bit clearer. This is the most compact example of the basic structure I've been playing with.. (forgive the coffeescript if that's not your thing).
angular.module 'app'
.controller 'KeyFormCtrl', (SessionService, $scope, $location, $log) ->
#error = null
#message = null
#keySent = false
#creds =
username: null
#sendKey = ->
SessionService.sendKey({ username: #creds.username })
.then(
(resp) =>
#keySent = true
(err) =>
#error = err.error
)
.directive 'eaKeyForm', ->
{
restrict: 'AE'
scope: {}
templateUrl: 'session/key-form.html'
replace: true
controller: 'KeyFormCtrl'
controllerAs: 'kfc'
bindToController: true
}
session/key-form.html:
<form>
<div ng-show="kfc.error">{{kfc.error}}</div>
<div ng-show="kfc.message">{{kfc.message}}</div>
<div ng-show="kfc.keySent">
An account key has been sent to {{kfc.creds.username}}.<br />
</div>
<div ng-hide="kfc.keySent">
<input type="email" ng-model="kfc.creds.username">
<button ng-click="kfc.sendKey()">Send Key</button>
</div>
</form>
angular.module('myApp', [])
.directive('myAweseomDirective', ['MyAwesomeService', function(MyAwesomeService) {
return {
link: function(scope) {
scope.saveFormDetails = function() {
MyAweseomeService.saveInformation(scope.data);
//where data is the ng-model for the form
}
}
};
}])
.service('MyAweseomService', function() {
MyAwesomeService.saveInformation = function(data) {
//do whatever you want to with the data
}
});