ngClass doesn't apply changes in parent directive - javascript

i made two directives, one's exposing an API for another directive using controller.
The child directive is a 'bodyElement' directive, and when clicked should update a class of the parent directive template.
While the modification of the parent $scope applies, the ngClass switch doesn't apply.
Hope you can help:
Directives:
.directive('humanBody', function () {
return {
transclude : true,
scope: {},
templateUrl: 'view1/template/human-body.tpl.html',
controller: ['$scope', function ($scope) {
$scope.form = {};
$scope.body = {};
$scope.body.selection = {};
$scope.body.selection.head = true;
$scope.body.selection.arm = false;
$scope.body.selection.chest = false;
$scope.body.selection.leg = false;
$scope.isActive = function (type) {
return $scope.body.selection[type];
};
this.toggle = function (type) {
$scope.body.selection[type] = !$scope.body.selection[type];
}
}]
}
})
.directive('bodyPart', function () {
return {
transclude : true,
scope: {
type: '#'
},
require: '^humanBody',
link: function (scope, elem, attr, humanBody) {
elem.on('click', function (event) {
console.info('toggle ' + scope.type);
humanBody.toggle(scope.type);
});
}
}
});
template of parent directive:
i need that isActive(type) in ngClass switch between no-background <-> type-container when toggling (false/true).
It just work when rendering the page.
<div class="container">
<div class="row col-xs-12 body-part-container body-container">
<div class="col-xs-12 "
ng-class="{'no-background': !isActive('head'), 'head-container':isActive('head')}">
<div class=" col-xs-12 arm-container"
ng-class="{'no-background': !isActive('arm'), 'arm-container':isActive('arm')}">
<div class="col-xs-12 chest-container"
ng-class="{'no-background': !isActive('chest'), 'chest-container':isActive('chest')}">
<div class="col-xs-12 leg-container container"
ng-class="{'no-background': !isActive('leg'), 'leg-container':isActive('leg')}">
<body-part type="head" class="head col-xs-12"></body-part>
<body-part type="arm" class="arm col-xs-4"></body-part>
<body-part type="chest" class="chest col-xs-4"></body-part>
<body-part type="arm" class="arm col-xs-4"></body-part>
<body-part type="leg" class="leg col-xs-12"></body-part>
</div>
</div>
</div>
</div>
</div>
</div>

You need to kick off digest cycle in bodyPart directive, as you are updating scope variable from customEvent(updating angular context from outside world wouldn't intimate angular to run digest cycle to update view level bindings).
Code
elem.on('click', function (event) {
console.info('toggle ' + scope.type);
humanBody.toggle(scope.type);
scope.$apply();
});

Related

Binding to a transclude in AngularJS

I'm not confident what I'm trying to do is correct, so please give me a "more correct" option if the whole premise is wrong.
I have an AngularJS 1.5.11 application. I am trying to create a generic modal html tag that can be used throughout the application wherever a modal is required. I have a page template that looks like this:
<form class="page-sidebar-container" data-name="mainForm">
<div class="page-sections">
<div class="form-columned form-columned-1">
<div class="form-row">
<div class="section column column-0 span-0" id="section-JobFromType">
<h4 class="section-heading">From Type</h4>
<div class="columns gutters-large">
<div style="display: table;margin: 18px 0px;">
<api-select label="Type" items="fromTypeItems" item="selectedFromType" required>
</api-select>
</div>
</div>
</div>
<div class="section column column-1 span-0" id="section-JobToType">
<h4 class="section-heading">To Type</h4>
<div class="columns gutters-large">
<div style="display: table;margin: 18px 0px;">
<api-select label="Type" items="toTypeItems" item="selectedToType"
read-only="toTypeDisabled" required>
</api-select>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<api-modal is-open="entitySelectOpen" label="{{entitySelectLabel}}">
<modal-body>
{{$id}}
<api-select label="{{entitySelectParentLabel}}" items="entitySelectParentItems" item="selectedEntitySelectParent" required>
</api-select>
<api-select label="{{entitySelectChildLabel}}" items="entitySelectChildItems" item="selectedEntitySelectChild" read-only="entitySelectChildDisabled" required>
</api-select>
<api-select label="{{entitySelectEntityLabel}}" items="entitySelectEntityItems" item="selectedEntity" read-only="entitySelectEntityDisabled" required>
</api-select>
</modal-body>
<modal-actions>
{{$id}}
<api-button label="{{modalPrimaryActionLabel}}" type="primary" icon="icon-checkmark" on-click="modalPrimaryActionClick"></api-button>
<api-button label="Return" type="return" icon="icon-cancel" on-click="modalReturnActionClick"></api-button>
</modal-actions>
</api-modal>
with the following controller:
(function (angular) {
angular.module('views.jobcontrol', [
'ngRoute',
'components.formApiControlSelect',
'components.formApiControlText',
'components.formApiModal'
])
.config([
'$routeProvider',
function ($routeProvider) {
'use strict';
var route = {
templateUrl: 'modules/apiViews/jobcontrol/jobcontrol-view.html',
controller: 'JobControlController',
reloadOnSearch: false,
caseInsensitiveMatch: true
};
$routeProvider
.when('/view/jobcontrol/:action/:jobNumber/?', route);
}
]).controller('JobControlController', [
'$scope',
'$timeout',
'$routeParams',
function ($scope, $timeout, $routeParams) {
'use strict';
function generateMockGuid() {
var result, i, j;
result = '';
for (j = 0; j < 32; j++) {
if (j == 8 || j == 12 || j == 16 || j == 20)
result = result + '-';
i = Math.floor(Math.random() * 16).toString(16).toUpperCase();
result = result + i;
}
return result;
function option(label,value){
return {
value:value,
label:label
}
}
$scope.fromTypeItems = [option("test",1)];
$scope.selectedFromType = null;
$scope.$watch('selectedFromType', function (){
console.log("do something");
});
/* to type */
$scope.fromTypeItems = [option("test",1)];
$scope.selectedToType = null;
$scope.toTypeDisabled = true;
$scope.$watch('selectedToType', function () {
console.log("do something else");
});
/* entity select modal */
$scope.selectedFromEntity = null;
$scope.selectedToEntity = null;
$scope.entitySelectParentItems = [option("parent 1", generateMockGuid()),option("parent 2", generateMockGuid()),option("parent 3", generateMockGuid())];
$scope.entitySelectChildItems = [option("child 1", generateMockGuid()),option("child 2", generateMockGuid()),option("child 3", generateMockGuid())];
$scope.entitySelectEntityItems = [option("entity 1", generateMockGuid()),option("entity 2", generateMockGuid()),option("entity 3", generateMockGuid())];
$scope.selectedEntity = null;
$scope.$watch('selectedEntity', function () {
console.log('selectedEntity has changed to ' + $scope.selectedEntity);
});
function clearModalSelections(){
console.log($scope);
$scope.selectedEntitySelectParent = null;
$scope.selectedEntitySelectChild = null;
$scope.selectedEntity = null;
}
$scope.modalPrimaryActionLabel = "Next";
$scope.modalPrimaryActionClick = function(){
clearModalSelections();
$scope.entitySelectOpen = false;
}
$scope.modalReturnActionClick = function(){
clearModalSelections();
$scope.entitySelectOpen = false;
}
}
]);
})(window.angular);
The modal works, it has my api-button's and api-select's in it, and the api-select's items are populated properly with the 3 options added to the $scope.entitySelectParentItems, $scope.entitySelectChildItems and $scope.entitySelectEntityItems arrays. The problem is that
$scope.$watch('selectedEntity', function () {
console.log('selectedEntity has changed to ' + $scope.selectedEntity);
});
never gets called. If I move the three api-select's out of the api-modal > modal-body and up the same level as the api-modal then everything works as expected (other than he elements being on the base page, rather than in the modal), but as soon as everything is inside the modal I lose access to when the user selects a value and what that value is.
I'm sure this must be something to do with the transclude having a seperate scope, but I'm confused. How can the api-select in the modal transclude access the main scope for its items to populate a select list and even updates if the list updates, but when the user selects something it isn't bound back to the scope the same way it does for the other api-select's on the page. Am I using ng-transclude wrong? Is this an OK thing to do, but I am missing a step?
In case it matters, the javascript for api-modal looks like this:
angular.module('components.formApiModal',[
'components.formApiButton'
])
.directive('apiModal', ['$timeout', '$compile', function ($timeout, $compile) {
'use strict';
return {
restrict: 'E',
templateUrl: 'modules/apiComponents/generic/modal/modal-template.html',
controller: 'apiModalController',
transclude: {
'body': 'modalBody',
'actions': 'modalActions'
},
scope: {
isOpen: "=",
label: '#',
actions: "="
},
link: function (scope, element, attrs) {
}
};
}])
.controller('apiModalController', ['$scope', function($scope) {
}]);
and the template:
<div class="modal-backdrop modal-large" data-ng-class="{'modal-closed' : !isOpen }">
<div class="modal-container">
<div class="modal" data-ng-click="$event.stopPropagation()">
<h1 class="modal-header" >{{label}}</h1>
<div class="modal-body">
<div class="form-messages" data-ng-if="formPage.overrideConfirmationMessages.length">
<div class="form-header-errors">
</div>
</div>
<h4 class="section-heading" data-ng-if="section.altHelp">{{section.altHelp}}</h4>
<div class="columns gutters-large">
<div ng-transclude="body"></div>
</div>
</div>
<div class="modal-footer" >
<div ng-transclude="actions"></div>
</div>
</div>
</div>
</div>

Angular directive dynamic template not working with bindings

This is a simplified version of a real problem but I want to understand what is happening and how to fix it. I have a directive that should load different templates deppending on a parameter that is being bind to it.
var app = angular.module('my_app', [])
.controller('controller', Controller)
.directive('dirTwo', DirTwo);
function Controller() {
var este = this;
this.list = ["number", "text", "text"];
this.change = function() {
this.list = this.list.reverse();
}
}
function DirTwo() {
return {
restrict: 'E',
//template: ' Name: {{name}}{{type}}',
template: my_template,
scope: {
type: "="
},
link: function($scope) {
$scope.name = "Pepito";
console.log("scope type: " + $scope.type);
}
}
}
var my_template = function(elem, attrs, $scope) {
console.log("template type: " + attrs.type);
switch (attrs.type) {
case 'number':
return '{{type}}';
break;
default:
return ' ---- '
break;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js"></script>
<div class="app" ng-app="my_app" ng-controller="controller as App">
<p>{{App.list}}
<button ng-click="App.change()">Reverse</button>
</p>
<dir-two ng-repeat="item in App.list track by $index" type="item"></dir-two>
</div>
The log in the template function prints the word item instead of number or text. How can I fix it to load the template correctly?
Unfortunately there is no way to use template like that because the template resolution happens before the compile phase where your type would assume a value on scope. To change the template of a directive based on a scope value, you must watch that value and recompile the directive element when it gets changed. It will recompile the directive entirely with a new template but with the same scope. You could do this once (i.e., without the $watch) as well.
The following snippet implements this solution.
var app = angular.module('my_app', [])
.controller('controller', Controller)
.directive('dirTwo', DirTwo);
function Controller() {
var este = this;
this.list = ["number", "text", "text"];
this.change = function() {
this.list = this.list.reverse();
}
}
function DirTwo($compile) {
return {
restrict: 'E',
template: '',
scope: {
type: "="
},
link: function(scope, element) {
var tmplScope;
scope.name = "Pepito";
scope.$watch('type', function(type) {
if (tmplScope) tmplScope.$destroy();
tmplScope = scope.$new();
element.html(getTemplate(type));
$compile(element.contents())(tmplScope);
});
}
}
}
var getTemplate = function(type) {
switch (type) {
case 'number':
return '{{type}}';
break;
default:
return ' ---- '
break;
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.1/angular.min.js"></script>
<div class="app" ng-app="my_app" ng-controller="controller as App">
<p>{{App.list}}
<button ng-click="App.change()">Reverse</button>
</p>
<dir-two ng-repeat="item in App.list track by $index" type="item"></dir-two>
</div>
UPDATE NOTE: I've changed the actual compile system to create a new child scope every time it gets compiled and also destroy this child scope when it gets recompiled to prevent adding new watchers when recompiling it without discarding the previous ones. Thanks to #georgeawg who warnned about this issue in the comments.

Why isolated scope "&" is not working with controllerAs in directive of angularJs

index.html
<div ng-app="phoneApp">
<div ng-controller="ctrl as AppCtrl">
<div phone dial="ctrl.callHome('called home!')"></div>
</div>
</div>
app.js :
var app = angular.module('phoneApp', []);
app.controller("AppCtrl", function ($scope) {
var ctrl = this;
ctrl.callHome = function (message) {
alert(message);
};
});
app.directive("phone", function () {
return {
scope: {},
bindToController : {
dial: "&"
},
controller : controller,
controllerAs : 'controllerVM',
templateUrl : 'node.html'
};
function controller(){
controllerVM.onClick = function(){
controllerVM.dial();
}
}
});
node.html :
<button ng-click="controllerVM.onClick()">Button</button>
when i click on Button of phone directive, it should call the function callHome of AppCtrl, but it is not getting called.
When i removes controllerAs from every where and calls the directive dial function from template directly, it is working properly
So please help me what am i missing?
Thanks in advance :)
Your code has 2 problems. In your HTML, you are using 'ctrl as AppCtrl' which is wrong. You should use 'AppCtrl as ctrl'. The second problem is within phone controller, you have not defined controllerVM. You should define it like 'var controllerVM = this;' I have inserted a working snippet below.
var app = angular.module('phoneApp', []);
app.controller("AppCtrl", function ($scope) {
var ctrl = this;
ctrl.callHome = function (message) {
alert(message);
};
});
app.directive("phone", function () {
return {
scope: {},
bindToController : {
dial: "&?"
},
controller : controller,
controllerAs : 'controllerVM',
template : '<button ng-click="controllerVM.onClick()">Button</button>'
};
function controller(){
var controllerVM = this;
controllerVM.onClick = function(){
controllerVM.dial();
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
<div ng-app="phoneApp">
<div ng-controller="AppCtrl as ctrl">
<div phone dial="ctrl.callHome('called home!')"></div>
</div>
</div>

How dynamically add new input element if all others was filled in AngularJS

please watch this Plunker
So I working with angular and need to add new input field when all others are filled in (by default on page placed 5 inputs and if all of them are filled automatically add one more input if new input also using will add one more input and etc).
For generate inputs I use ng-repeat and name_list[] for it:
<div collect-input>
<div class="form-group" ng-repeat="(i, name) in name_list track by $index">
<div class="row">
<div class="col-xs-12">
<input class="form-control" type="text" ng-model="data.name_list[i]" add-input/>
</div>
</div>
</div>
Each input have directive attr "add-input" with $watch() method inside. This method method track when $isEmpty parameter had changed.
Then value function pass value of this parameter to listen function.
directive('addInput', ['$compile', '$sce', '$timeout', function ($compile, $sce, $timeout) {
return {
restrict: 'A',
require: ['^collectInput', '?ngModel'],
link: function (scope, element, attrs, ctrl) {
var collectInput = ctrl[0];
var ngModel = ctrl[1];
$timeout(function(){
scope.$watch(
function(){
return ngModel.$isEmpty(ngModel.$modelValue);
},
function(isEmpty){
collectInput.reportInput(ngModel, isEmpty);
}
);
},1000)
}
}
}]);
Then this function call "reportInput()" that placed inside parent directive "collect-input". Main goal of this function is to add new input name to name_list[] for generating via ng-repeat
userApp.directive('collectInput', function() {
return {
restrict: 'A',
controller: function($scope) {
var dirtyCount = 0;
this.reportInput = function(modelValue, isEmpty) {
var count = $scope.name_list.length;
if (isEmpty == false){
dirtyCount ++;
console.log('+1:' + dirtyCount);
}
if (isEmpty == true){
if (dirtyCount <= 0){
dirtyCount = 0;
console.log('0:' + dirtyCount);
}
else if(dirtyCount > 0){
dirtyCount --;
console.log('-1:' + dirtyCount)
}
}
if (count === dirtyCount) {
$scope.name_list.push(modelValue);
//dirtyCount = dirtyCount + 1;
}
console.log('count:' + count);
console.log('dirtyCount:' + dirtyCount);
console.log(modelValue)
}
},
link: function(scope, element, attrs) {
}}});
So when I filled 5 default inputs everything is good after it appears new input but it is all in my IDE it work perfect if I add only one symbol for 5+ label (in plunker in some reason it not work) but when I add or delete something more code logic crash. It's hard to explain. I hope Plunker code more clarify this.
Not tested, and could be optimized, but here's my idea:
HTML :
<div class="form-group" ng-repeat="name in name_list">
<div class="row">
<div class="col-xs-12">
<input class="form-control" ng-model="name"/>
</div>
</div>
</div>
JS :
//watch any modification in the list of names
$scope.$watchCollection('data.name_list', function (list) {
//is there an empty name in the list?
if (!list.filter(function (name) { return !name; }).length) {
//if not, let's add one.
data.name_list.push('');
//and that will automatically add an input to the html
}
});
I don't see the point of a directive.

The attributes passed to directive in AngularJS change only into directive scope but not outside

I want to use a directive to customize my code.
I have created a button to switch isCollapsedUpload flag defined in the controller as: #scope.isCollapsedUpload=false.
When the user presses the button, the isCollapsedUpload turns to true or vice versa and the icon changes.
From the controller:
$scope.switcher = function (booleanExpr, trueValue, falseValue) {
return booleanExpr ? trueValue : falseValue;
}
$scope.isCollapsedUpload = false;
<button class="btn" ng-click="isCollapsedUpload = !isCollapsedUpload">
<span>Upload file</span>
<i class="{{ switcher( isCollapsedUpload, 'icon-chevron-right', 'icon-chevron-down' )}}"></i>
</button>
I wrote this directive:
feederliteModule.directive('collapseExtend', function() {
return {
restrict: 'E',
scope: { isCollapsed:'#collapseTarget' },
compile: function(element, attrs)
{
var htmlText =
'<button class="btn" ng-click="isCollapsed = !isCollapsed">'+
' <span>'+attrs.label+'</span>'+
' <i class="{{ switcher(isCollapsed, \'icon-chevron-right\', \'icon-chevron-down\' )}}"></i>'+
'</button>';
element.replaceWith(htmlText);
}
}
});
And now I can use it like:
<collapse-extend
collapse-target="isCollapsedUpload"
label="Upload file"
></collapse-extend>
It doesn't work. No icon changes. No errors,
isCollapsedUpload flag doesn't change. It changes only into directive
Did I miss something?
The reason the class doesn't change correctly is because you are not linking the template properly. This is easy to fix if you use the built in functionality:
var feederliteModule = angular.module('feederliteModule', []);
feederliteModule.directive('collapseExtend', [function() {
return {
restrict: 'E',
scope: {
isCollapsed:'=collapseTarget',
label: '#'
},
template: '<button class="btn" ng-click="isCollapsed = !isCollapsed">'+
'<span>{{ label }}</span>'+
'<i ng-class="{ \'icon-chevron-right\': isCollapsed, \'icon-chevron-down\': !isCollapsed }"></i>'+
'</button>'
}
}]);
feederliteModule.controller('test', ['$scope', function($scope) {
$scope.isCollapsedUpload = false;
}]);
To the best of my understanding, by replacing the parent element, you were removing the isolate scope this object was tied to without creating a new one on the button itself.
EDIT: See a complete working fiddle with multiple buttons
I suggest using a service instead of a controller to maintain your model data. This allows you better separation of concerns as your app gets more complex:
var feederliteModule = angular.module('feederliteModule', []);
feederliteModule.service('btnService', function(){
this.isCollapsedUpload = false;
this.isCollapsedSomething = false;
});
feederliteModule.controller('btnController', function($scope, btnService){
$scope.isCollapsedUpload = btnService.isCollapsedUpload;
$scope.isCollapsedSomething = btnService.isCollapsedSomething;
});
feederliteModule.directive('collapseExtend', function() {
return {
restrict: 'E',
scope: {
isCollapsed:'=collapseTarget',
label:'#'
},
replace: true,
link: function (scope, element, attrs){
scope.switcher = function (booleanExpr, trueValue, falseValue) {
return booleanExpr ? trueValue : falseValue;
};
scope.toggleCollapse = function() {
scope.isCollapsed = !scope.isCollapsed;
}
},
template: '<button class="btn" ng-click="toggleCollapse()">'+
'<span>{{label}}</span>'+
'<i ng-class="switcher(isCollapsed, \'icon-chevron-right\', \'icon-chevron-down\')"></i>'+
'</button>'
}
});
Also, notice that you must use '=' instead of '#' in order for isCollapsed to work as you expect. The answer above needs this as well.

Categories

Resources