AngularJS - Why is the DOM not updating - angularjs

So, what I am trying to do is add a "stack" - a basic JS object - into an array called cardStacks, declared the stackCtrl function. I've read that the controller in AngularJS is not supposed to do very much, nor do you want the manipulation of DOM elements done the controller. So what I have done is create a "save" directive which, as you could probably guess, adds a new "stack" into $scope.cardStacks (if the object is not being edited)
Everything seems to work ok, up until when the template is supposed to update. console.log() reveals that objects are going into an array, but because the template is not being updated, I can only guess it is not $scope.cardStacks.
Can somebody give the following a look and tell me why the template is not listing the "stacks" in the template?
Consider the following:
Template:
<div ng-controller="stackCtrl">
<stackeditor></stackeditor>
<p>Stacks:</p>
<ul class="stack-list" ng-repeat="stack in cardStacks">
<li>{{stack.title}}
<span class="stack-opts"> (Edit | Delete)</span>
</li>
</ul>
</div>
Template for the stackeditor tag/directive:
<div>
<span class="button" ng-click="show=!show">+ New</span>
<form ng-show="show" novalidate>
<input type="text" ng-model="stackTitle" required/>
<button class="button" save>Save</button>
<button class="button" reset>Cancel</button>
<div ng-show="error"><p>{{error}}</p></div>
</form>
</div>
Controller:
function stackCtrl($scope) {
$scope.cardStacks = [];
$scope.stackTitle = "";
$scope.addStack = function(title) {
var newStack = {};
newStack.title = title;
$scope.cardStacks.push(newStack);
}
$scope.removeStack = function($index) {
console.log("removing Stack...");
}
$scope.editStack = function(element) {
console.log("editing Stack...");
}
}
Directive:
fcApp.directive('save', function() {
var linkFn = function($scope, element, attrs) {
element.bind("click", function() {
if (typeof $scope.stackTitle !== 'undefined' && $scope.stackTitle.length > 0) {
if ($scope.edit) {
$scope.editStack(element);
} else {
$scope.addStack($scope.stackTitle);
}
} else {
$scope.error = "Your card stack needs a title!";
}
});
});
return {
restrict: "A",
link: linkFn
}
}
});

Try using $apply:
$scope.cardStacks.push(newStack);
$scope.$apply(function(){
$scope.cardStacks;
}
Rendering might be the problem..... Hope it helps.

The save function would be better in stackCtrl and then use ng-click in the template to call it.
You are right that manipulating the DOM in the controller is bad practice, but you are just updating an object in the controller - angular is sorting out the DOM which is fine.

Related

Dynamically adding multiple custom directives associated with single controller in angularjs 1

I have an html template attached to a controller and three directives. There are three buttons. On clicking a button a directive is added to the page(using ng-click) like this(the following is in my controller not in directive):
$scope.addFilterDimension = function() {
console.log('CLICKED!!!!!!!')
var divElement = angular.element(document.querySelector('#filterDimension'));
var appendHtml = $compile('<filter-directive></filter-directive>')($scope);
divElement.append(appendHtml);
}
Similarly for other buttons, other directives are added. Now, I can keep adding as many of these directives as I like, which is the use case here.
These directives are basically like forms containing either dropdowns, input boxes or both. The values user selects from the dropdowns or enters in input boxes have to be sent back to the backend to be stored in the DB.
This is one of the directives(others are very similar):
anomalyApp.directive('filterDirective', function() {
return {
restrict: "E",
scope: {},
templateUrl:'filter-dimension.html',
controller: function($rootScope, $scope, $element) {
$scope.dimensionsOld = $rootScope.dimensionsOld;
$scope.dimensions = $rootScope.dimensions;
$scope.selectedDimensionName = $rootScope.selectedDimensionName;
$scope.selectedDimensionValue = $rootScope.selectedDimensionValue;
$scope.extend = $rootScope.extend;
$scope.selectedExtend = $rootScope.selectedExtend;
$scope.isDateField = $rootScope.isDateField;
console.log($scope.dimensions);
$scope.Delete = function(e) {
//remove element and also destoy the scope that element
$element.remove();
$scope.$destroy();
}
}
}
});
Now, in my controller I assign $rootscope to my values which have to be used in the directives and thus catch them in the directive. Example:
$rootScope.dimensions = temp.map(d=>d.dimName);
$rootScope.selectedDimensionName = '';
$rootScope.selectedDimensionValue = '';
And this is how I retrieve my values from added directives:
var retriveValue = function() {
var filtersData = [];
var constraintsData = [];
var variablesData = [];
var ChildHeads = [$scope.$$childHead];
var currentScope;
while (ChildHeads.length) {
currentScope = ChildHeads.shift();
while (currentScope) {
if (currentScope.dimensions !== undefined){
filtersData.push({
filterDimensionName: currentScope.selectedDimensionName,
filterDimensionValue: currentScope.selectedDimensionValue,
filterDimensionExtend: currentScope.selectedExtend,
filterDimensionIsDateFiled: currentScope.isDateField
});
}
if (currentScope.constraintDimensions !== undefined){
filtersData.push({
constraintDimensionName: currentScope.selectedConstraintName,
constraintDimensionValue: currentScope.selectedConstraintValue,
constraintDimensionExtend: currentScope.selectedConstraintExtend,
constraintDimensionVariable: currentScope.selectedConstraintVariable,
constraintDimensionOperator: currentScope.selectedOperator,
constraintDimensionVariableValue: currentScope.constraintVariableValue,
constraintDimensionIsDateField: currentScope.isDateFieldConstraint
});
}
if (currentScope.variableNames !== undefined){
console.log('currentScope.selectedVariableVariable',currentScope.selectedVariableVariable);
filtersData.push({
variableName: currentScope.selectedVariableVariable,
variableOperator: currentScope.selectedVariableOperator,
variableValue: currentScope.variableVariableValue,
variableExtend: currentScope.selectedVariableExtend
});
}
currentScope = currentScope.$$nextSibling;
}
}
return filtersData;
}
This is one of the directive's template:
<div >
<div>
<label>Dimension</label>
<select class = "custom-select custom-select-lg mb-6" ng-model="selectedDimensionName" ng-options="dimension for dimension in dimensions">
<!-- <option ng-repeat="table in tables track by $index">{{table}}</option> -->
</select>
</div>
<div>
<label>Date Field</label>
<input type="checkbox" ng-model="isDateField">
</div>
<div>
<label>Value</label>
<select multiple class = "custom-select custom-select-lg mb-6" ng-model="selectedDimensionValue" ng-options="val for val in ((dimensionsOld | filter:{'dimName':selectedDimensionName})[0].dimValues)"></select>
</span>
</div>
<div>
<label>Extend</label>
<select class = "custom-select custom-select-lg mb-6" ng-model="selectedExtend" ng-options="val for val in extend"></select>
</span>
</div>
<button type="button" class="btn btn-danger btn-lg" ng-click="Delete($event)">Delete</button>
This is in the main html to add the directive:
<div id="filterDimension"> </div>
I know this is not a good way, but please suggest a better one.
Secondly, a new change has to be made where inside one of the directives there will be a button, clicking on which 2 dropdowns(or simply one more directive) will be added and can be added as many times as needed(just like the other directive).
The issue here is this one is a directive inside another directive and I am facing unusual behavior like:
When I add the parent directive it is fine, I add the child directives its fine, but when I add a second parent and try to add its child, they get appended inside the first directive.
Even if I somehow manage to get out of the above point I do not know how to retrieve values from such directives.
PS: I am new in AngularJS or front-end for that matter, the retriveValue() and using rootscope I got from somewhere online.

how to watch changes on hidden fields with Angular?

I have dropdown made from divs. It's not my doing, is from external agency. But this is not important. The div with a dropdown looks like this:
<label class='label-block' for='kraj-dokument'>Kraj wydania dokumentu tożsamości *</label>
<input type='hidden' name='kraj-dokument' id='kraj-dokument' value="" drop-down-validation-directive /> <div class="select kraj-dokument" data-destination="kraj-dokument">
<p class="label">Wybierz z listy</p>
<div class="options">
<p class="option" data-ng-repeat="country in countries">{{country.Name}}</p>
</div>
</div>
When you click on a choice in this dropdown an external javascript is adding th the hidden field attribute called value and it insert a text from the choise to that value. As you can see I have a directive on that hidden inputu which looks like this (after several searches, and everything):
myapp.directive("dropDownValidationDirective", function () {
return function(scope, elem, attr, ctrl) {
scope.$watch(attr['value'], function(nv) {
elem.val(nv);
}
);
}
});
The problem is when I'm doing watch nothing happens, the value is not geting watched although the change is seen when debuging this in chrome. By the way. I'm trying to also do a validation on this dropdowwn. My idea was to check if this value is filled or not and tell to the user to fill the dropdown by adding a class to it wich marks this thing red. Is that the way to do it?
UPDATE I forgot to add, and I think this is also important that the click event on that div is done with the mentioned external javascript. I'm pasting it below. I've put the external javascript functionality into the service:
myApp.service('DropDownService', function () {
this.renderDropDown = function () {
function initEvents() {
var selectClicked = $(".selectClicked");
$(".select").each(function () {
var destination = $(this).attr("data-destination");
var option = $(this).find(".option");
var options = $(this).find(".options");
var label = $(this).find(".label");
var select = $(this);
label.click(function () {
if (label.hasClass("clicked")) {
$(".select .options").hide();
$(".select .label").removeClass("clicked");
$(".select").removeClass("clicked");
selectClicked.removeClass("clicked");
} else {
$(".select .label").removeClass("clicked");
$(".select").removeClass("clicked");
label.addClass("clicked");
select.addClass("clicked");
selectClicked.addClass("clicked");
$(".select .options").hide();
options.show();
}
});
option.unbind("click").bind("click", function () {
$("#" + destination).attr("value", $(this).text());
label.text($(this).text());
options.hide();
$(".select .label").removeClass("clicked");
$(".select").removeClass("clicked");
$(".select").removeClass("error");
selectClicked.removeClass("clicked");
});
});
}
angular.element(document).ready(function () {
initEvents();
if (navigator.appVersion.indexOf("Mac") !== -1) {
$('body').addClass("MacOS");
}
});
}
Just use interpolation & attrs.$observe instead:
HTML:
<div ng-controller="AppController">
<label class="label-block" for="kraj-dokument">Kraj wydania dokumentu tożsamości *</label>
<input type="hidden" name="kraj-dokument" id="kraj-dokument" value="{{ TotalPrice }}" drop-down-validation-directive />
<div class="select kraj-dokument" data-destination="kraj-dokument" ng-click="AddItem()">
Click to change and see console output
</div>
</div>
JS:
var app = angular.module('my-app', [], function() {})
app.controller('AppController', function($scope) {
$scope.Quantity = 0;
$scope.TotalPrice = 0;
$scope.Price = 100;
$scope.AddItem = function() {
$scope.Quantity++;
$scope.TotalPrice = $scope.Price * $scope.Quantity;
};
});
app.directive("dropDownValidationDirective", function() {
return {
link: function(scope, elem, attrs, ctrl) {
attrs.$observe('value', function(nv) {
console.log(nv);
});
}
}
});
Working example: http://jsfiddle.net/ghd9c8q3/56/
I'm not sure the way you're doing things is the best way, but I know why your watch doesn't do anything.
$scope.$watch() expects either a function, or an expression.
If you pass a function, it's called at each digest loop and its result is the value passed to the change listener.
If you pass an expression, it's evaluated on the scope, and the result of the evaluation is the value passed to the change listener.
So what you actually need is:
scope.$watch(function() {
return attr['value'];
}, function(nv) {
elem.val(nv);
});

Teaxarea have inherent scope when i click on it is collapsing but don't let me to type my comment?

I have created already my code in here . There is three directives which each of responsible to collapse somethings.
If you execute this code you will see there is 2 panel where if you click on address is collapsing. Also if you click on comment is collapsing too. However my problem came when i want to type some comment inside textarea then is going to collapse as well. This is not what i need. I would like to click on add comment [+], then is collapsing and start to type whatever i want. I define inherent scope in script.js. I am not sure how i can change my code in order to solve this problem. I will be thank you if any of you can help me.
This is add comment directive
.directive('addComment', function() {
return {
restrict: "E",
templateUrl: "addComment.html",
scope:true,
controller: function($scope) {
$scope.collapsed = false;
$scope.collapseCom = function() {
$scope.collapsed = true;
}
$scope.extendCom = function() {
$scope.collapsed = false;
}
}
}
})
and this is my html.
<div ng-show="!collapsed" ng-click=collapseCom()>
<h4>Add Comments: [+]<br /></h4>
<div class="form-group">
<div class="col-lg-10">
<textarea name="comment" ng-model="comment" class="form-control" rows="3" required></textarea>
{{comment}}
</div>
</div>
</div>
<div ng-show="collapsed" ng-click=extendCom()>
<h4>Add Comments: [-]</h4>
</div>
To see all the details please click on following link.
replace ng-click=collapseCom()
with ng-click="collapseCom()"
$scope.collapseCom = function() {
$scope.collapsed = true;
}
$scope.extendCom = function() {
$scope.collapsed = false;
}
replace one method
$scope.collapseCom = function() {
if($scope.collapsed){
$scope.collapsed = false;
}else{
$scope.collapsed = true;
}
}
This works like switch then, and should work fine

AngularJS input with focus kills ng-repeat filter of list

Obviously this is caused by me being new to AngularJS, but I don't know what is the problem.
Basically, I have a list of items and an input control for filtering the list that is located in a pop out side drawer.
That works perfectly until I added a directive to set focus to that input control when it becomes visible. Then the focus works, but the filter stops working. No errors. Removing focus="{{open}}" from the markup makes the filter work.
The focus method was taken from this StackOverflow post:
How to set focus on input field?
Here is the code...
/* impersonate.html */
<section class="impersonate">
<div header></div>
<ul>
<li ng-repeat="item in items | filter:search">{{item.name}}</li>
</ul>
<div class="handle handle-right icon-search" tap="toggle()"></div>
<div class="drawer drawer-right"
ng-class="{expanded: open, collapsed: !open}">
Search<br />
<input class="SearchBox" ng-model="search.name"
focus="{{open}}" type="text">
</div>
</section>
// impersonateController.js
angular
.module('sales')
.controller(
'ImpersonateController',
[
'$scope',
function($scope) {
$scope.open = false;
$scope.toggle = function () {
$scope.open = !$scope.open;
}
}]
);
// app.js
angular
.module('myApp')
.directive('focus', function($timeout) {
return {
scope: { trigger: '#focus' },
link: function(scope, element) {
scope.$watch('trigger', function(value) {
if(value === "true") {
console.log('trigger',value);
$timeout(function() {
element[0].focus();
});
}
});
}
};
})
Any assistance would be greatly appreciated!
Thanks!
Thad
The focus directive uses an isolated scope.
scope: { trigger: '#focus' },
So, by adding the directive to the input-tag, ng-model="search.name" no longer points to ImpersonateController but to this new isolated scope.
Instead try:
ng-model="$parent.search.name"
demo: http://jsbin.com/ogexem/3/
P.s.: next time, please try to post copyable code. I had to make quite a lot of assumptions of how all this should be wired up.

How do I use a directive to toggle a slide animation on an element from my controller?

I am confused on the following scenario. Let's say I have a table with rows. When a user clicks a button in the table I want a user form to slide down with jQuery and display the form with the selected row values. Here is what I am currently doing that doesn't quite make sense:
View
<tr ng-click="setItemToEdit(item)" slide-down-form>
...
<form>
<input type="test" ng-model={{itemToEdit.Property1}} >
<button ng-click=saveEditedItem(item)" slide-up-form>
<form>
Control
$scope.itemToEdit = {};
$scope.setItemToEdit = function(item) {
$scope.itemToEdit = item;
});
$scope.saveEditedItem = function(item) {
myService.add(item);
$scope.itemToEdit = {};
}
Directive - Slide-Up / Slide-Down
var linker = function(scope, element, attrs) {
$(form).slideUp(); //or slide down
}
It seems the my directive and my control logic are too disconnected. For example, what happens if there is a save error? The form is already hidden because the slideUp event is complete. I'd most likely want to prevent the slideUp operation in that case.
I've only used AngularJS for about a week so I'm sure there is something I'm missing.
Sure, it's a common problem... here's one way to solve this: Basically use a boolean with a $watch in a directive to trigger the toggling of your form. Outside of that you'd just set a variable on your form to the object you want to edit.
Here's the general idea in some psuedo-code:
//create a directive to toggle an element with a slide effect.
app.directive('showSlide', function() {
return {
//restrict it's use to attribute only.
restrict: 'A',
//set up the directive.
link: function(scope, elem, attr) {
//get the field to watch from the directive attribute.
var watchField = attr.showSlide;
//set up the watch to toggle the element.
scope.$watch(attr.showSlide, function(v) {
if(v && !elem.is(':visible')) {
elem.slideDown();
}else {
elem.slideUp();
}
});
}
}
});
app.controller('MainCtrl', function($scope) {
$scope.showForm = false;
$scope.itemToEdit = null;
$scope.editItem = function(item) {
$scope.itemToEdit = item;
$scope.showForm = true;
};
});
markup
<form show-slide="showForm" name="myForm" ng-submit="saveItem()">
<input type="text" ng-model="itemToEdit.name" />
<input type="submit"/>
</form>
<ul>
<li ng-repeat="item in items">
{{item.name}}
<a ng-click="editItem(item)">edit</a>
</li>
</ul>

Resources