AngularJS ES6 parent function not being called by child component - javascript

I'm a bit new to the ES6 syntax/structure of Angular 1.x, and I'm running into an issue with passing a function from a parent controller to a child controller.
This is how the app is tied together (I use webpack + babel with this as the entry-point):
const requires = [
'ngRoute',
];
//App
angular.module('kanban',requires)
.controller('dashboardCtrl', dashboardCtrl)
.component('board', board)
.component('task', task)
.config(routes);
In my routes, I have a single route, which is my 'parent'
export default function($routeProvider) {
$routeProvider
.when('/', {
template: dashboardTemplate,
controller: 'dashboardCtrl',
controllerAs: '$ctrl',
});
}
Who's controller looks like this:
export default function($rootScope) {
$rootScope.title = 'Kanban';
let _this = this;
this.boards = [
{
_id: 'b1',
title: 'backlog',
tasks: ['t1', 't2'],
}
];
this.deleteBoard = function(board) {
console.log(board);
let index = _this.boards.indexOf(board);
if (index !== -1) {
_this.boards.splice(index, 1);
}
};
And in the template, the child is created with ng-repeat, passing in the function
<board ng-repeat="board in $ctrl.boards" board="board" onDelete="$ctrl.deleteBoard(board)" ></board>
And the board binds the attribute as a function with an &
export const board = {
template: boardTemplate,
controller: boardCtrl,
bindings: {
board: '=',
onDelete: '&',
}
};
And the function is added to the controller within a different function:
export default function boardCtrl() {
let _this = this;
this.deleteBoard = function(){
console.log(_this.onDelete);
_this.onDelete({board: _this.board});
};
}
And called with a click:
<button ng-click="$ctrl.deleteBoard()"></button>
I can reach the board (child) controller's function, which prints this in the console:
function (locals) {
return parentGet(scope, locals);
}
And returns no errors, but the console.log in the parent deleteBoard function does not get called.
What is happening here? Why does the child seem to recognize that it is calling something in the parent scope, but is not reaching it?

Turns out this issue was because of how the attribute was named in the parent template, where
<board onDelete="$ctrl.deleteBoard(board)"></board>
needed to be the following instead:
<board on-delete="$ctrl.deleteBoard(board)"></board>
even though the attribute is bound as "onDelete" on the child controller.

Related

Two way binding angular component with parent controller

I have an angular component (vendor-filters) that I would like to pass data to and from a parent controller. The purpose is to create a filter off of mainInfo, and pass that data back to the parent controller where it will reflect the filtering in the component. My problem is that this mainInfo variable is returning undefined in the component controller. Here's my code :
html
<div ng-controller="kanban as ctrl">
<vendor-filters mainInfo="ctrl.mainInfo"></vendor-filters>
<div class="qt-kb">
<kanban-list label="Incoming" type="incoming" cards="ctrl.mainInfo.incoming"></kanban-list>
<kanban-list label="In Progress" type="progress" cards="ctrl.mainInfo.inProgress"></kanban-list>
<kanban-list label="Waiting For Documentation" type="docs" cards="ctrl.mainInfo.documentation"></kanban-list>
<kanban-list label="Pending Approval" type="closed" cards="ctrl.mainInfo.closed"></kanban-list>
</div>
Parent Controller :
app.controller("kanban", ["$scope", "assignmentDataService", "globalSpinner", function ($scope, assignmentDataService, globalSpinner) {
const vm = this;
vm.mainInfo = [];
activate();
function activate() {
getData();
}
function getData() {
var promise = assignmentDataService.getData()
.then(function(data) {
vm.mainInfo = data;
});
globalSpinner.register(promise);
};
}]);
Component controller:
class VendorFilterCtrl {
constructor($http, $scope, $timeout,assignmentDataService) {
this.$scope = $scope
this.$http = $http;
const vm = this;
//I could be initializing this wrong but this is where I'm trying to get
//the data.
vm.data = vm.mainInfo;
}
app.controller('kanban').component("vendorFilters", {
templateUrl: "app/components/vendorFilters.html",
bindings: {
store: "=?store",
onChange: '&',
mainInfo: '<'
},
controller: VendorFilterCtrl,
controllerAs: "ctrl",
bindToController: true
});
Basically I'm trying to get the mainInfo from the parent controller into the component and visa versa. Any idea why this isn't working?
Start by using kebab-case for the attribute:
<vendor-filters ̶m̶a̶i̶n̶I̶n̶f̶o̶ main-info="ctrl.mainInfo"></vendor-filters>
NEXT
Fix this:
app ̶.̶c̶o̶n̶t̶r̶o̶l̶l̶e̶r̶(̶'̶k̶a̶n̶b̶a̶n̶'̶)̶ .component("vendorFilters", {

Load service data into angular component

I have a component which takes care of drawing two lists, but in the component there is no data so nothing is drawn.
myController
function loadAllData() {
Admin.getAllSettings()
.then(function (settings) {
$scope.settings = settings.data;
})
}
myComponent
{
bindings: {
selectedData: '=',
availableData: '<'
},
templateUrl: 'global/twoListSelector.directive.html',
controller: function () {
var me = this;
console.log(me);
}
}
myView
<two-side-selector selectedData="doctorProperties" availableData="settings"></two-side-selector>
In the console.log the output for me.settings is undefined. Shouldn't the digest cycle update the setting property so it gets to the component? The service is returning data correctly but it is not getting to the component
I am using angular 1.5.9
Try to use selected-data and available-data attributes at markup:
<two-side-selector selected-data="doctorProperties" available-data="settings"></two-side-selector>
AngularJS convert dash-separated attributes to camel-case by itself

Angular Directive to component angular 1.5

I have this directive, that i would like to make a component
angular.module('app')
.directive('year', function () {
var controller = ['$scope', function ($scope) {
$scope.setYear = function (val) {
$scope.selectedyear = val;
}
}];
return {
restrict: 'E',
controller: controller,
templateUrl: "views/year.html"
};
});
This is what I got so far:
angular.module('app')
.component('year', {
restrict: 'E',
controller: controller,
templateUrl: "views/year.html"
});
I am not sure how to move my var controller into .component
There are few things you should do convert your directive to component
There is no restrict property for component as it is restricted to elements only.
For controller you could just set as you did at directive declaration but outside of it.
Controllers for components use controllerAs syntax as default so get rid of $scope
So your component should look like this...
angular.module('app')
.component('year', {
controller: ComponentController,
templateUrl: "views/year.html"
});
function ComponentController(){
var $ctrl = this;
$ctrl.setYear = function (val) {
$ctrl.selectedyear = val;
}
}
Your component should look like this:
function YearController() {
var $ctrl = this;
$ctrl.setYear = function (val) {
$ctrl.selectedyear = val;
}
}
angular.module('app').component('year', {
templateUrl: 'views/year.html',
controller: YearController
});
For more details, please read the following Stack Overflow question for more deatils:
Angular 1.5 component vs. old directive - where is a link function?
and the official documentation:
https://docs.angularjs.org/guide/component
Also, please note that components are restricted to elements only by default.

Angular service that is instantiated on every page change/request

So, I'm making my first steps in AngularJS (1.5) and I'm trying to build a feature that will let me change few things in my layout based on the route.
As far as I understood I needed a service for this. Basically the setup I have is:
'use strict';
/* App Module */
var app = angular.module('app', [
'ngRoute',
'appControllers',
'AppServices'
]);
app.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/', {
template: '<h1>Home page</h1>',
controller: 'MainController'
}).when('/page', {
template: '<h1>Page</h1>',
controller: 'PagesController'
}).otherwise({
redirectTo: '/'
});
}]);
var appControllers = angular.module('appControllers', []);
appControllers.controller('MainController', ['$rootScope', 'AppSetup', function($scope, AppSetup) {
$scope.app = AppSetup.build();
console.log('home');
}]);
appControllers.controller('PagesController', ['$rootScope', 'AppSetup', function($scope, AppSetup) {
AppSetup.setProperties({
meta: {
title: 'My Second Page'
}
});
console.log('page');
$scope.app = AppSetup.build();
}]);
var AppServices = angular.module('AppServices', []);
AppServices.service('AppSetup', [function() {
var properties = {
meta: {
title: 'My App • Best of the best'
}
},
styles;
this.setProperties = function(input) {
this.properties = angular.extend(properties, input);
};
//TODO: This will override app-wide styles.
this.setStyles = function(input) {
this.styles = angular.extend({}, input);
};
this.build = function() {
return {
properties: properties,
styles: styles
};
};
}]);
Plunkr here
So I have one defined properties object and want to override it when I visit a page. The problem is that when I go back to home, it doesn't set the default value. Obviously it's instantiated once the page is loaded and then remains the same until changed.
What's the best approach to do this?
I have tried adding a listener to the route, as #Raul A. suggested, but it's not working. Output from console:
Plunkr here
You can use the $routeChangeSuccess event if you are using routing and make changes in the function watching for it:
$rootScope.$on("$routeChangeSuccess", function(currentRoute, previousRoute){
//Do you changes here
});

Angular ui-router resolve value as string

With ui-router, I add all resolve logic in state function like this;
//my-ctrl.js
var MyCtrl = function($scope, customers) {
$scope.customers = customers;
}
//routing.js
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: { // <-- I feel this must define as like controller
customers: function(Customer, $stateParams) {
return Customer.get($stateParams.id);
}
}
});
However IMO, resolve object must belong to a controller, and it's easy to read and maintain if it is defined within a controller file.
//my-ctrl.js
var MyCtrl = function($scope, customers) {
$scope.customers = customers;
}
MyCtrl.resolve = {
customers: function(Customer, $stateParams) {
return Customer.get($stateParams.id);
};
};
//routing.js
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: 'MyCtrl.resolve' //<--- Error: 'invocables' must be an object.
});
However, When I define it as MyCtrl.resolve, because of IIFE, I get the following error.
Failed to instantiate module due to: ReferenceError: MyCtrl is not defined
When I define that one as string 'MyCtrl.resolve', I get this
Error: 'invocables' must be an object.
I see that controller is defined as string, so I think it's also possible to provide the value as string by using a decorator or something.
Has anyone done this approach? So that I can keep my routings.js clean and putting relevant info. in a relevant file?
It sounds like a neat way to build the resolve, but I just don't think you can do it.
Aside from the fact that "resolve" requires an object, it is defined in a phase where all you have available are providers. At this time, the controller doesn't even exist yet.
Even worse, though, the "resolve" is meant to define inputs to the controller, itself. To define the resolve in the controller, then expect it to be evaluated before the controller is created is a circular dependency.
In the past, I have defined resolve functions outside of the $stateProvider definition, at least allowing them to be reused. I never tried to get any fancier than that.
var customerResolve = ['Customer', '$stateParams',
function(Customer, $stateParams) {
return Customer.get($stateParams.id);
}
];
// ....
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: {
customers: customerResolve
}
});
This question is about features of ui-router package. By default ui-router doesn't support strings for resolve parameter. But if you look at the source code of ui-router you will see, that it's possible to implement this functionality without making direct changes to it's code.
Now, I will show the logic behind suggested method and it's implementation
Analyzing the code
First let's take a look at $state.transitionTo function angular-ui-router/src/urlRouter.js. Inside that function we will see this code
for (var l = keep; l < toPath.length; l++, state = toPath[l]) {
locals = toLocals[l] = inherit(locals);
resolved = resolveState(state, toParams, state === to, resolved, locals, options);
}
Obviously this is where "resolve" parameters are resolved for every parent state. Next, let's take a look at resolveState function at the same file. We will find this line there:
dst.resolve = $resolve.resolve(state.resolve, locals, dst.resolve, state);
var promises = [dst.resolve.then(function (globals) {
dst.globals = globals;
})];
This is specifically where promises for resolve parameters are retrieved. What's good for use, the function that does this is taken out to a separate service. This means we can hook and alter it's behavior with decorator.
For reference the implementation of $resolve is in angular-ui-router/src/resolve.js file
Implementing the hook
The signature for resolve function of $resolve is
this.resolve = function (invocables, locals, parent, self) {
Where "invocables" is the object from our declaration of state. So we need to check if "invocables" is string. And if it is we will get a controller function by string and invoke function after "." character
//1.1 Main hook for $resolve
$provide.decorator('$resolve', ['$delegate', '$window', function ($delegate, $window){
var service = $delegate;
var oldResolve = service.resolve;
service.resolve = function(invocables, locals, parent, self){
if (typeof(invocables) == 'string') {
var resolveStrs = invocables.split('.');
var controllerName = resolveStrs[0];
var methodName = resolveStrs[1];
//By default the $controller service saves controller functions on window objec
var controllerFunc = $window[controllerName];
var controllerResolveObj = controllerFunc[methodName]();
return oldResolve.apply(this, [controllerResolveObj, locals, parent, self]);
} else {
return oldResolve.apply(this, [invocables, locals, parent, self]);
}
};
return $delegate;
}]);
EDIT:
You can also override $controllerProvider with provider like this:
app.provider("$controller", function () {
}
This way it becomes possible to add a new function getConstructor, that will return controller constructor by name. And so you will avoid using $window object in the hook:
$provide.decorator('$resolve', ['$delegate', function ($delegate){
var service = $delegate;
var oldResolve = service.resolve;
service.resolve = function(invocables, locals, parent, self){
if (typeof(invocables) == 'string') {
var resolveStrs = invocables.split('.');
var controllerName = resolveStrs[0];
var methodName = resolveStrs[1];
var controllerFunc = $controllerProvider.getConstructor(controllerName);
var controllerResolveObj = controllerFunc[methodName]();
return oldResolve.apply(this, [controllerResolveObj, locals, parent, self]);
} else {
return oldResolve.apply(this, [invocables, locals, parent, self]);
}
};
Full code demonstrating this method http://plnkr.co/edit/f3dCSLn14pkul7BzrMvH?p=preview
You need to make sure the controller is within the same closure as the state config. This doesn't mean they need to be defined in the same file.
So instead of a string, use a the static property of the controller:
resolve: MyCtrl.resolve,
Update
Then for your Controller file:
var MyCtrl;
(function(MyCtrl, yourModule) {
MyCtrl = function() { // your contructor function}
MyCtrl.resolve = { // your resolve object }
yourModule.controller('MyCtrl', MyCtrl);
})(MyCtrl, yourModule)
And then when you define your states in another file, that is included or concatenated or required after the controller file:
(function(MyCtrl, yourModule) {
configStates.$inject = ['$stateProvider'];
function configStates($stateProvider) {
// state config has access to MyCtrl.resolve
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: MyCtrl.resolve
});
}
yourModule.config(configStates);
})(MyCtrl, yourModule);
For production code you will still want to wrap all these IIFEs within another IIFEs. Gulp or Grunt can do this for you.
If the intention is to have the resolver in the same file as the controller, the simplest way to do so is to declare the resolver at the controller file as a function:
//my-ctrl.js
var MyCtrl = function($scope, customers) {
$scope.customers = customers;
}
var resolverMyCtrl_customers = (['Customer','$stateParams', function(Customer, $stateParams) {
return Customer.get($stateParams.id);
}]);
//routing.js
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'MyCtrl',
resolve: resolverMyCtrl_customers
});
This should work.
//my-ctrl.js
var MyCtrl = function($scope, customer) {
$scope.customer = customer;
};
//routing.js
$stateProvider
.state('customers.show', {
url: '/customers/:id',
template: template,
resolve: {
customer: function(CustomerService, $stateParams){
return CustomerService.get($stateParams.id)
}
},
controller: 'MyCtrl'
});
//service.js
function CustomerService() {
var _customers = {};
this.get = function (id) {
return _customers[id];
};
}

Categories