Turn On ng-repeat element when loading repeater - angularjs

I have a typical ng-repeat as you'll see below:
<button ng-repeat="tube in safeConfig.tubes" on-repeat-done="tubes_done" ng-class="{'btn btn-on' : tube.toggled, 'btn btn-off' : !tube.toggled}" ng-click="toggleBtn(tube, 'tube', $index)">{{tube.label}}</button>
As you can see when the button is clicked it runs a function to toggle the button on. It also adds an item to an array that is used in a custom filter to filter out items from a separate ng-repeat element.
I'm trying to figure out how to have the buttons in the ng-repeat already turned on when the page loads. The directive I have attached to the ng-repeat item is being called. the filterData service loads the array that holds the items that are filtering the list below:
secure.directive("toggleButtonOnLoad", ['filterData', function (filterData) {
return {
restriction: 'A',
scope: {tube: '='},
link: function ($scope, element, attributes) {
var filterItems = filterData.getFilterItems();
if ($scope.$last) {
$scope.$emit(attributes["onRepeatDone"] || "repeat_done", element);
}
}
}
}]);
Is there a way to access the individual button and turn it on with the structure I've started? It seems wrong that filterItems would get called with every button, maybe extra unnecessary work. I need to essentially do something like item.toggled on each item if it is in the array.
When I step through it I'm trying to access element[0].innerText() to compare it with items in the array. And it just shows the binding {{tube.label}} Any way to show it evaluated?
Any thoughts or direction would be appreciated.

I ended up adding two attributes on my HTML
device='tube' toggle-button-on-load=''
Full HTML below:
<button ng-repeat="tube in safeConfig.tubes" device='tube' toggle-button-on-load='' ng-class="{'btn btn-on' : tube.toggled, 'btn btn-off' : !tube.toggled}" ng-click="toggleBtn(tube, 'tube', $index)">{{tube.label}}</button>
The directive gives me access to the device
secure.directive("toggleButtonOnLoad", ['filterData', 'utility', function (filterData, utility) {
return {
restriction: 'A',
scope: { device: '=' },
link: function ($scope, element, attr) {
var filterItems = filterData.getFilterItems();
if (filterItems.indexOf(utility.squish($scope.device.label)) !== -1) {
$scope.device.toggled = true;
}
}
}
}]);
I can now access the object itself getting bound to the element and turn the toggle on/off depending on the arry returned from my service getFilterItems().

Related

AngularJs Directive don't update itself for source element's attribute updates

tried almost everything but not succeed.
I wanted to take an action only if I click on an element whose class name is "ion-android-favorite-outline".
My source element is like below
<i id="{{product.product_code}}" class="icon ion-android-favorite-outline"></i>
I am able to do it using "restrict:'C'" in the directive code.
In the directive, once I am done with data processing, I am changing source element's class to "ion-android-favorite" as below.
.directive(
"ionAndroidFavoriteOutline",
function(sessionStorageService, productService){
return{
restrict: 'C',
link: function(scope, element, attrs) {
element.bind("click" , function(e){
var productCode = attrs.id;
console.log(attrs.class); // on every click i get class name as 'ion-android-favorite-outline' event though html has class name as 'ion-android-favorite'
if (sessionStorageService.isLoggedIn()) {
productService.trackProduct(productCode).then(function(response){
if (response.data.status == "success") {
element.removeClass("ion-android-favorite-outline").addClass("ion-android-favorite");
} else {
element.removeClass("ion-android-favorite").addClass("ion-android-favorite-outline");
}
});
scope.$apply();
}
I can see source element's class name getting changed and its reflection in the UI as well.
However, if I again click on the source element (whose class is changed in previous click action ) still invokes the directive code.
Why ???
I tried scope.$apply , replace: true etc ... but no workaround.
Plz help.
You can use directive like below:
<i id="{{product.product_code}}" class="ion-android-favorite-outline" ion-android-favorite-outline></i>
#stephan, answered correctly.
element.unbind("click");

Angular Animation Directive

I'm trying to write an angular directive that will animate a list of words similar to http://codepen.io/rachsmith/pen/BNKJme . However, I'm needing to load the text from a json file and then select a random sentence to apply the animation to. I have this part working, but am having trouble accessing the directive's child elements. I am assuming this is because the directive is being called before the elements are rendered, but using scope.$on($viewContentLoaded, function... has not made a difference.
I have jQuery and Underscore available.
Here is my code:
Controller
Data.sentences().then(function (response) {
var sentences = response.data;
$scope.sentence = _.sample(sentences);
});
View
<div class="rotator">
<p>{{sentence.static}}</p>
<text-rotator>
<span class="word" ng-repeat="item in sentence.options">{{item}}</span>
</text-rotator>
</div>
Directive
app.directive('textRotator', function () {
return {
restrict: 'E',
link: function (scope, el, attrs) {
var words = el.children('.word');
//cannot access array of items with class of word
}
};
});
Your assumption is correct, the ng-repeat-ed words are not yet in the DOM at the time the link function of the directive is executed. The sentence object is fetched asynchronously.
Listening on $viewContentLoaded won't help: this is an event fired by ngRoute module when the content of the ngView is loaded. After a digest cycle followed by DOM updates due to a change on the model, this event is not fired.
Actually, I think you're creating yourself troubles as the data could be (should be) passed as a parameter to the directive. The child word elements would be the template of the directive. I suggest something like the following:
app.directive('textRotator', function () {
return {
restrict: 'E',
scope: {
options: '='
},
templateUrl: 'words.html',
link: // ...
}
});
Template:
<text-rotator options="sentence.options"></text-rotator>
This fiddle might help you. The animation part has been replaced by a simple toggling of the opacity. Also, the words are mocked in the controller, you should make sure they are resolved by the router in the definition of the route / state, or otherwise you would have to add a watcher in the directive.

<option> ele dynamically added in directive's link function not firing parent select's ngChange

I need to add <option> elements dynamically based on certain property values of the source data populating the dropdown list. Sometimes the child elements need to instead be <optgroup> elements due to the crazy way marketing wants data displayed. Due to the requirements, I cannot do simple statements with ng-options nor can I do a parent <select> with a child element combined with ng-repeat. I run into various issues in my directive trying to use ng-switch or ng-if to get the desired result.
Because of those issues I decided to put the entire select element as the template in my directive. Then in the link function I would parse out all the incoming data and append the correctly formatted <option> or <optgroup> elements to the parent <select> defined in the template. I started with the scaled down code in my directive below and was going to expand upon it with my conditional logic when I noticed changing the selected option in the UI was not firing the parent select's ng-change function in my controller. The ng-change does fire when I let Angular add all the options itself with ng-options or when I use child option element with ng-repeat. However, breaking it out the way shown below with a minimal parent defined in the template and then dynamically adding child elements in link does not work.
app.directive('fullSelect', function ($compile) {
return {
restrict: 'E',
scope: { datarepo: "=datarepo" },
replace: true,
template: "<select class='col-xs-8' id='gridStyle' " +
'ng-model="vm.gridStyle" ng-change="vm.gridStyleUpdated()"></select>',
link: function (scope, element, attrs) {
angular.forEach(scope.datarepo, function (value, key) {
var opt = angular.element('<option value="' + value.value + '">' + value.label + '</option>');
element.append($compile(opt)(scope));
});
}
}
});
I additionally tried adding an ng-click to each of the newly added <option> elements during the forEach loop, but even those do not fire. I assume this is all some sort of scope/visibility issue.
Thanks in advance for any guidance.
please try this:
link: function (scope, element, attrs) {
scope.vm = {};
scope.vm.gridStyleUpdated = function () {
console.log("changed");
}
angular.forEach(scope.datarepo, function (value, key) {
var opt = angular.element('<option value="' + value.value + '">' + value.label + '</option>');
element.append($compile(opt)(scope));
});
}
I finally got what I wanted by breaking up my directive a little and putting a big chunk of it back into the html view. Now the events are correctly firing and I also get additional benefit of nested option and optgroup elements to any level which is something that started this whole cycle. Here is what the view html looks like now:
<fieldset id="fs_gridStyle">
<label class="col-xs-4" for="gridStyle">Grid Style *</label>
<select class="col-xs-8" id="gridStyle"
ng-model="vm.gridStyle"
ng-change="vm.gridStyleUpdated()"
full-select datarepo="vm.gridStyles">
</select>
</fieldset>
And here is what the directive looks like:
app.directive('fullSelect', function ($compile) {
return {
restrict: 'A',
scope: { datarepo: "=datarepo" },
replace: true,
link: function (scope, element, attrs) {
angular.forEach(scope.datarepo, function (value, key) {
var opt;
var display = "";
for (var idx = 0; idx < value.level; idx++) {
display += " ";
}
display += value.label;
if (value.type === "Option") {
opt = angular.element('<option value="' + value.value + '">' + display + '</option>');
}
else {
opt = angular.element('<optgroup label="' + display + '"></optgroup>');
}
element.append($compile(opt)(scope));
});
}
}
});
Now I just need to make sure it performs properly, but so far it looks good. It gives me my desired end result where my incoming object array contains values, labels and misc info such as a level property to indicate where that item should be indented within the dropdown list, etc. I can now have any number of option and optgroup children embedded within the dropdown list indented as needed. You cannot nest multiple optgroup elements, but I visually handle that with the level property which adds spaces to the text.

AngularJS directives and require

I have this directive that is getting more and more complicated. So I decided to split it up into parts.
The directive itself loaded a garment SVG graphic, when the SVG loaded it then ran a configure method which would apply a design, applied picked colours (or database colours if editing) and other bits and pieces.
As I said, it was all in one directive, but I have now decided to separate the logic out.
So I created my first directive:
.directive('configurator', function () {
// Swap around the front or back of the garment
var changeView = function (element, orientation) {
// If we are viewing the front
if (orientation) {
// We are viewing the front
element.addClass('front').removeClass('back');
} else {
// Otherwise, we are viewing the back
element.addClass('back').removeClass('front');
}
};
return {
restrict: 'A',
scope: {
garment: '=',
onComplete: '&'
},
require: ['configuratorDesigns'],
transclude: true,
templateUrl: '/assets/tpl/directives/kit.html',
link: function (scope, element, attrs, controllers) {
// Configure our private properties
var readonly = attrs.hasOwnProperty('readonly') || false;
// Configure our scope properties
scope.viewFront = true;
scope.controls = attrs.hasOwnProperty('controls') || false;
scope.svgPath = 'assets/garments/' + scope.garment.slug + '.svg';
// Apply the front class to our element
element.addClass('front').removeClass('back');
// Swaps the design from front to back and visa versa
scope.rotate = function () {
// Change the orientation
scope.viewFront = !scope.viewFront;
// Change our view
changeView(element, scope.viewFront);
};
// Executes after the svg has loaded
scope.loaded = function () {
// Call the callback function
scope.onComplete();
};
}
};
})
This is pretty simple in design, it gets the garment and finds the right SVG file and loads it in using ng-transclude.
Once the file has loaded a callback function is invoked, this just tells the view that it is on that it has finished loading.
There are a few other bits and pieces that you should be able to work out (changing views, etc).
In this example I am only requiring one other directive, but in the project there are 3 required directives, but to avoid complications, one will suffice to demonstrate my problem.
My second directive is what is needed to apply the design. It looks like this:
.directive('configuratorDesigns', function () {
return {
restrict: 'A',
controller: 'ConfiguratorDesignsDirectiveController',
link: function (scope, element, attrs, controller) {
// Get our private properties
var garment = scope.$eval(attrs.garment),
designs = scope.$eval(attrs.configuratorDesigns);
// Set our controller designs array
controller.designs = designs;
// If our design has been set, watch it for changes
scope.$watch(function () {
// Return our design
return garment.design;
}, function (design) {
// If we have a design
if (design) {
// Change our design
controller.showDesign(element, garment);
}
});
}
}
})
The controller for this directive just loops through the SVG and finds the design that matches the garment design object. If it finds it, it just hides the others and shows that one.
The problem I have is that this directive is unaware of the SVG loading or not. In the "parent" directive I have the scope.loaded function which is executed when the SVG has finished loading.
The "parent" directive's template looks like this:
<div ng-transclude></div>
<div ng-include="svgPath" onload="loaded()"></div>
<span class="glyphicon glyphicon-refresh"></span>
So my question is this:
How can I get the required directives to be aware of the SVG loaded state?
If I understand your question correctly, $rootScope.broadcast should help you out. Just broadcast when the loading is complete. Publish a message from the directive you are loading the image. On the directive which needs to know when the loading is complete, listen for the message.

How can I be notified when DOM elements are added to my directive?

I've got a simple directive that draws a few elements, like in this example. I want to programatically set some style properties but in the link function, the elements are apparently not there yet.
Here's a fiddle.
What I think is happening is that when I call the colorSquares function, there are no squares yet in the DOM. Wrapping it in a $timeout, it works, but that just feels so wrong.
Is there any way I can be notified when the elements exist? Or is there a place that I can put the code which will access them that is guaranteed to run after they exist?
myApp.directive('myDirective', ['$timeout', function ($timeout) {
return {
restrict: 'E',
replace: false,
link: function (scope, elem, attr) {
scope.squares = [1,2,3,4,5];
function colorSquares() {
var squaresFromDOM = document.getElementsByClassName('square');
for (var i = 0; i < squaresFromDOM.length; i++) {
squaresFromDOM[i].style['background-color'] = '#44DD44';
}
}
// this does not work, apparently because the squares are not in the DOM yet
colorSquares();
// this works (usually). It always works if I give it a delay that is long enough.
//$timeout(colorSquares);
},
template: '<div><div ng-repeat="s in squares" class="square"></div></div>'
};
}]);
You should work with Angular rather than against it which is to say you should use data bindings to do what you are trying to do rather than events/notifications in this context.
http://jsfiddle.net/efdwob3v/5/
link: function (scope, elem, attr) {
scope.squares = [1,2,3,4,5];
scope.style = {"background-color": "red"};
},
template: '<div><div ng-repeat="s in squares" class="square" ng-style="style"></div></div>'
That said there's no difference in doing the above and just using a different class that has that red background color or even just doing style="background-color: red;"
you put the answer in your qeustion, "It always works if I give it a delay that is long enough.".
So just make the delay long enough, in this situation that can be achieved by adding an onload event because when the elements get added to the DOM it calls that event.
So instead of just colorSquares(); you could use:
window.addEventListener("load", colorSquares);
Though this may not be the ideal solution since it will also trigger when something else triggers the onload event.
Answering your question directly. To know if an element is added to a directive or to the DOM in general, you can simply put a directive on that element, since the directive will run only when the element on which it "sits" is already in the DOM.
Using part of your code as an example:
myApp.directive('myDirective', function () {
return {
...
//put custom directive that will notify when DOM is ready
template: '<div><div ng-repeat-ready ng-repeat="s in squares" class="square"></div></div>'
};
});
And here is the custom ng-repeat-ready directive:
myApp.directive('ngRepeatReady', function () {
return {
link: function (scope) {
if (scope.$last) {
//do notification stuff here
//for example $emit an event
scope.$emit('ng-repeat is ready');
}
}
}
});
This directive will run when the element on which is sits is already in the DOM and check if the element has $last property on the scope (ng-repeat sets this flag for the last element of the iterated object) which means that the ng-repeat directive is done and you can now operate on the DOM safely.

Resources