Dynamically Add/Remove Attribute Directive based on state - javascript

I would like to build a directive that adds and remove a second directive based on a boolean flag.
So, when boolFlag = true, the div in example.html should recompile to include my-dir as an attribute. And it should have additional attributes required by my-dir in the form of "test=1".
Here is the basics of what I have so far:
example.html ----
<div add-remove when={{boolFlag}} dir-name="my-dir" dir-attrs="test, 1"></div>
add-remove-directive.js ----
function($compile, $timeout) {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
var attrArray = attrs.dirAttrs.split(',');
for (a in attrArray) {
attrArray[a] = attrArray[a].trim();
}
attrs.$observe('when', function() {
// flagged when the directive should be attached to the element
var isWhen = attrs.when === 'true';
if (isWhen) {
for (a = 0; a < attrArray.length; a+=2) {
elem.attr(attrArray[a], attrArray[a+1]);
}
newScope = scope.$new();
cElem = $compile(elem)(newScope);
elem.replaceWith(cElem);
$timeout(function() {
scope.$destroy();
});
}
// if the flag is not set, remove the dynamic directive from the element
// but only if the element already has the given directive
else {
normDirName = attrs.$normalize(attrs.dirName);
console.log(normDirName);
console.log(r, 'elsed', attrs);
if (!attrs.hasOwnProperty(normDirName)) return;
elem.removeAttr(scope.dirName);
for (a = 0; a < attrArray.length; a+=2) {
elem.removeAttr(attrArray[a]);
}
newScope = scope.$new();
cElem = $compile(elem)(newScope);
elem.replaceWith(cElem);
$timeout(function() {
scope.$destroy();
});
}
}
}
---EDIT---
Sorry, that was an error with me copying it over from my project. It is functional up to how I described in the original post.
Basically where it's at now is I can dynamically "my-dir" directive correctly with the corresponding required attributes "test=1".
But the problems I'm running into are:
that there are two scopes active the original one without "my-dir" and another scope WITH "my-dir".
my else statement doesn't properly remove "my-dir" from the div when boolFlag is set to false.

Related

Angular: Next section shown before previous section hidden - using ng-show

I'm implementing a reusable step-by-step wizard directive in angular based on this example. It's working pretty well, but like in the example, I'm using ng-show to hide all steps but the current one. This results in a quick flicker whenever I change steps where both the current and next step is shown simultaneously. What I can't figure out is how to do away with the flicker and make sure only a single step is shown at any one time.
What I've tried:
My own initial attempt at solving this issue was to change the show/hide-mechanic to use ng-switch but it doesn't work well since the ng-switch-when directive only accepts strings (so I can't populate it automatically with an index). Furthermore, ng-switch-when works through translcusion meaning I would have 2 transclusion directives on a single element which doesn't really make sense.
The wizard-directive as I've currently implemented it looks like this:
// Wizard
// ======
//
// This component implements a wizard-directive and a dependent step-directive
// which together can be used to define a step-by-step wizard with arbitrary
// html in each step.
//
// Ex:
// ```html
// <wizard>
// <step>
// <h1>Step one</h1>
// </step>
// <step>
// <h1>Step two</h1>
// </step>
// </wizard>
// ```
//
angular.module('wizard', [])
// Wizard Directive
// ----------------
//
// The main directive which defines the wizard element. A wizard can contain
// arbitrary html, but will only display a single step-element at a time. The
// directive also defines a couple of ways to navigate the wizard - through
// buttons and bottom "tabs".
//
.directive('wizard', function($rootScope) {
return {
restrict: 'E',
transclude: true,
scope: {},
templateUrl: $rootScope.templateBasePath + '/components/wizard/wizard.html',
controller: function($scope, $element) {
// Initialize the array of steps. This will be filled by any child
// steps added to the wizard.
var steps = $scope.steps = [];
// Search through the wizard to find what step is currently visible.
function getCurrentStepIndex() {
var index;
angular.forEach(steps, function(step, i) {
if (step.selected) {
index = i;
// End early when the selected step is found.
return;
}
});
return index;
}
// Make the imagePath available to the template.
$scope.imagePath = $rootScope.imagePath;
// Move to the next step in the wizard.
$scope.next = function () {
var index = getCurrentStepIndex();
if (index < steps.length - 1) {
steps[index].selected = false;
steps[index+1].selected = true;
}
};
// Move to the previous step of the wizard.
$scope.previous = function () {
var index = getCurrentStepIndex();
if (index > 0) {
steps[index].selected = false;
steps[index-1].selected = true;
}
};
// Select a given step in the wizard.
$scope.select = function(step) {
angular.forEach(steps, function(step) {
step.selected = false;
});
step.selected = true;
};
$scope.onFirstStep = function() {
return getCurrentStepIndex() === 0;
}
$scope.onLastStep = function() {
return getCurrentStepIndex() === steps.length - 1;
}
// Called by the step directive to add itself to the wizard.
this.addStep = function(step) {
// Select the first step when added.
if (steps.length === 0) {
$scope.select(step);
}
// Add the step to the step list.
steps.push(step);
};
}
};
})
// Step Directive
// --------------
//
// The Step Directive defines a section of code which constitues a distinct step
// in the overall goal of the wizard. The directive can only exist as a direct
// child of a wizard-tag.
//
.directive('step', function() {
return {
require: '^wizard', // require a wizard parent
restrict: 'E',
transclude: true,
scope: true,
template: '<div class="wizard__step ng-hide" ng-show="selected"></div>',
link: function(scope, element, attrs, wizardCtrl, transclude) {
// Add itself to the wizard's list of steps.
wizardCtrl.addStep(scope);
// Make the wizard scope available under "wizard" in the transcluded
// html scope.
scope.wizard = scope.$parent.$parent;
// Transclude the tag content in order to set the scope. This allows
// the content to access the wizard's next() and previous() functions.
var transDiv = angular.element(element).find('.wizard__step');
transclude(scope, function (clone) {
transDiv.append(clone);
});
}
};
});
The corresponding wizard template looks like so:
<div class="wizard" tabindex="1">
<div class="wizard__display">
<div class="wizard__previous" ng-click="previous()"><div class="guideBack" ng-hide="onFirstStep()"></div></div>
<div class="wizard__content" ng-transclude></div>
<div class="wizard__next" ng-click="next()"><div class="guideNext" ng-hide="onLastStep()"></div></div>
</div>
<ul class="nav wizard__tabs">
<li ng-repeat="step in steps" ng-click="select(step)" ng-class="{active:step.selected}"></li>
</ul>
</div>
I was making it unnecessarily difficult for myself by focusing all my attention on making it work with ng-show or ng-switch. The problem is eliminated quickly if you just perform the showing and hiding manually through jqlite/jquery. After some refactoring, the wizard-code looks like this:
// Wizard
// ======
//
// This component implements a wizard-directive and a dependent step-directive
// which together can be used to define a step-by-step wizard with arbitrary
// html in each step.
//
// Ex:
// ```html
// <wizard>
// <step>
// <h1>Step one</h1>
// </step>
// <step>
// <h1>Step two</h1>
// </step>
// </wizard>
// ```
//
angular.module('ssbbtip.wizard', [])
// Wizard Directive
// ----------------
//
// The main directive which defines the wizard element. A wizard can contain
// arbitrary html, but will only display a single step-element at a time. The
// directive also defines a couple of ways to navigate the wizard - through
// buttons, bottom breadcrumbs and keyboard arrow keys.
//
.directive('wizard', function($rootScope) {
return {
restrict: 'E',
transclude: true,
scope: {},
templateUrl: $rootScope.templateBasePath + '/components/wizard/wizard.html',
controller: function($scope, $element) {
// Initialize the array of steps. This will be filled by any child
// steps added to the wizard.
var steps = $scope.steps = [],
// currentStep is the shadow variable supporting the
// `$scope.currentStep` property.
currentStep = 0;
// This utility function will adjust a value to fit inside the
// specified range inclusively.
function clampToRange(min, max, value) {
// Make sure the max is at least as big as the min.
max = (min > max) ? min : max;
if (value < min) {
return min;
} else if (value > max) {
return max;
} else {
return value;
}
}
// This property specifies the currently visible step in the wizard.
Object.defineProperty($scope, 'currentStep', {
enumerable: true,
configurable: false,
get: function () { return currentStep; },
set: function (value) {
if (value && typeof(value) === 'number') {
currentStep = clampToRange(0, steps.length-1, value);
} else {
currentStep = 0;
}
}
});
// Make the imagePath available to the template.
$scope.imagePath = $rootScope.imagePath;
// Handle keyboard events on the wizard to allow navigation by
// keyboard arrows.
$scope.onKeydown = function (event) {
event.preventDefault();
console.log(event);
switch (event.which) {
case 37: // left arrow
case 38: // up arrow
$scope.previous();
break;
case 39: // right arrow
case 40: // down arrow
case 32: // space bar
$scope.next();
break;
}
};
// Move to the next step in the wizard.
$scope.next = function () {
$scope.currentStep = $scope.currentStep + 1;
};
// Move to the previous step of the wizard.
$scope.previous = function () {
$scope.currentStep = $scope.currentStep - 1;
};
$scope.onFirstStep = function() {
return $scope.currentStep === 0;
};
$scope.onLastStep = function() {
return $scope.currentStep === steps.length - 1;
};
// Called by the step directive to add itself to the wizard.
this.addStep = function (step) {
steps.push(step);
};
// This watches the `$scope.currentStep` property and updates the UI
// accordingly.
$scope.$watch(function () {
return $scope.currentStep;
}, function (newValue, oldValue) {
$element.find('step .wizard__step').eq(oldValue).addClass('ng-hide');
$element.find('step .wizard__step').eq(newValue).removeClass('ng-hide');
});
}
};
})
// Step Directive
// --------------
//
// The Step Directive defines a section of code which constitues a distinct step
// in the overall goal of the wizard. The directive can only exist as a direct
// child of a wizard-tag.
//
.directive('step', function() {
return {
require: '^wizard', // require a wizard parent
restrict: 'E',
transclude: true,
scope: true,
template: '<div class="wizard__step ng-hide"></div>',
link: function(scope, element, attrs, wizardCtrl, transclude) {
// Add itself to the wizard's list of steps.
wizardCtrl.addStep(scope);
// Make the wizard scope available under "wizard" in the transcluded
// html scope.
scope.wizard = scope.$parent.$parent;
// Transclude the tag content manually in order to set the scope.
// This allows the content to access the `wizard.next()` and
// `wizard.previous()` functions.
var transDiv = angular.element(element).find('.wizard__step');
transclude(scope, function (clone) {
transDiv.append(clone);
});
}
};
});
and the template:
<div class="wizard" tabindex="1" ng-keydown="onKeydown($event)">
<!-- tabindex 1 is needed to make the div selectable in order to capture keydown -->
<div class="wizard__display">
<div class="wizard__previous" ng-click="previous()"><div class="guideBack" ng-hide="onFirstStep()"></div></div>
<div class="wizard__content" ng-transclude></div>
<div class="wizard__next" ng-click="next()"><div class="guideNext" ng-hide="onLastStep()"></div></div>
</div>
<ul class="nav wizard__tabs">
<li ng-repeat="step in steps track by $index" ng-click="currentStep = $index" ng-class="{active: currentStep === $index}"></li>
</ul>
</div>

angularjs textarea - how to keep it in sync with angularjs dropdowns selected values

I have a couple of drop downs and based on selection I updated the textarea content, but when I am trying to update the text the function, I presume gets triggered, and does not let me update.
Here is my html :
<textarea ng-model="desc" ng-change="des"></textarea>
and angular
somehow the code got deleted.. please visit plunker: http://plnkr.co/edit/pVSiNrnAOY0A5v0iTmYQ?p=preview
It is better to use watch functions, especially if values might change in the controller, as this ensures that they stay in sync. This solution also seems easier to see what is going on:
plunkr solution
Just using a watch function to watch the selected values:
$scope.$watch('selectedId', function(newValue){
$scope.d();
});
I am not sure if you want the user to be able to change the value in the textarea or not. If you don't, like in your example, then I suggest that you add the attribute ng-disabled="true" to your text area.
you also need
$scope.des = function() {
return $scope.d()
}
Not sure what your trying todo, but if have just plunked this
// Code goes here
app = angular.module('myapp',[]);
app.controller('main', function($scope) {
$scope.desc = "";
$scope.stat = "test";
$scope.d = function() {
for (var i = 0, len = $scope.stat.length; i < len; i++) {
if ($scope.stat[i].Id == $scope.selectedId && $scope.statDate == $scope.stat[i].StatDate) {
$scope.desc = $scope.stat[i].D;
return $scope.stat[i].D;
}
}
return "";
};
$scope.des = function() {
return $scope.d()
}
});
And I get your issue, if I comment out
// $scope.desc = $scope.stat[i].D;
it works, but I presume that bit you need :)
What are you trying todo? I think you are trying to change model as the view is changed. You might need a directive for this.
This is part of something I did for datetime stuff
.directive('formatteddate', function ($filter) {
return {
link: function (scope, element, attrs, ctrl) {
ctrl.$parsers.unshift(function (viewValue) {
This is the plunk of your code/html.
http://plnkr.co/edit/3GjP7YVsKFIYAAJQwmOK?p=preview
okay,so what about if you just update the ng-model value of textarea to the value of your select dropdown.
Here's the plunker
http://plnkr.co/edit/mXQTcBu5rOJaQJKEFKpm?p=preview
change the model name to some:
<textarea ng-model="textarea" ng-change="desc()"></textarea>
and then update in, in your script where your code logic is:
$scope.d = function() {
for (var i = 0, len = $scope.stat.length; i < len; i++) {
console.log(i);
if ($scope.stat[i].Id == $scope.selectedId && $scope.statDate == $scope.stat[i].StatDate) {
$scope.desc = $scope.stat[i].D;
**$scope.textarea=$scope.desc;**
return $scope.stat[i].D;
}
}
return "";
};

Angular auto trigger specific directive in ng-repeat

I have an interesting situation.
I have a directive with isolate scope that generate list of numbers and the user can choose numbers like in lottery.
The problem i have is that i required minimum of 1 line, if the user pick only one line so when he click play i want to auto trigger the next directive in the ng-repeat to pick for him numbers, I made this plunker so you guys can understand better and help me.
http://plnkr.co/edit/vWGmSEpinf7wxRUnqyWq?p=preview
<div ng-repeat="line in [0,1,2,3]">
<div line line-config="lineConfig">
</div>
</div>
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.lineConfig = {
guessRange: 10
}
$scope.lines = [];
$scope.$on('lineAdded', function(event, line) {
$scope.lines.push(line);
});
$scope.play = function() {
/// here i want to check if $scope.lines.length
//is less then one if yes then auto trigger the next
//line directive to auto do quick pick and continue
}
})
.directive('line', function() {
return {
restrict: 'A',
templateUrl: 'line.html',
scope: {
lineConfig: '='
},
link: function($scope, elem, attr) {
var guessRange = $scope.lineConfig.guessRange;
$scope.cells = [];
$scope.line = {
nums: []
};
$scope.$watch('line', function(lotLine) {
var finaLine = {
line: $scope.line
}
if ($scope.line.nums.length > 4) {
$scope.$emit('lineAdded', finaLine);
}
}, true);
_(_.range(1, guessRange + 1)).forEach(function(num) {
$scope.cells.push({
num: num,
isSelected: false
});
});
$scope.userPickNum = function(cell) {
if (cell.isSelected) {
cell.isSelected = false;
_.pull($scope.lotLine.nums, cell.num);
} else {
cell.isSelected = true;
$scope.lotLine.nums.push(cell.num);
}
};
$scope.quickPick = function() {
$scope.clearLot();
$scope.line.nums = _.sample(_.range(1, guessRange + 1), 5);
_($scope.line.nums).forEach(function(num) {
num = _.find($scope.cells, {
num: num
});
num.isSelected = true;
});
}
$scope.clearLot = function() {
_($scope.cells).forEach(function(num) {
num.isSelected = false;
});
$scope.line.nums = [];
}
}
}
})
You could pass the $index (exists automatically in the ng-repeat scope) - variable into the directive and cause it to broadcast an event unique for ($index + 1) which is the $index for the next instance.
The event could be broadcasted from the $rootScope or a closer scope that's above the repeat.
Then you could capture the event in there.
Probably not the best way to do it.
I can try to elaborate if anything is unclear.
EDIT
So I played around alittle and came up with this:
http://plnkr.co/edit/ChRCyF7yQcN580umVfX1?p=preview
Rather
Rather than using events or services I went with using a directive controller to act as the parent over all the line directives inside it:
.directive('lineHandler', function () {
return {
controller: function () {
this.lines = [];
}
}
})
Then requiring 'lineHandler' controller inside the 'line' directive - the controller being a singleton (same instance injected into all the line directives) - you can then setup that controller to handle communication between your directives.
I commented most of my code in the updated plnkr and setup an example of what I think you requested when clicking in one list - affecting the one beneath.
I hope this helps and if anything is unclear I will try to elaborate.

Creating a kendo-grid with reusable options using AngularJS

How to create a kendo-grid with reusable options using AngularJS?
Besides the default settings, the grid must include a checkbox column dynamically with the option to select all rows . Methods to treat the selections should be part of the directive and, somehow, I should be able to access the rows selected in controller.
Another important behavior is to keep a reference to the grid :
// In the controller : $scope.grid
<div kendo-grid="grid" k-options="gridOptions"></div>
Below an initial path that I imagined, but it is not 100% working because AngularJS not compile information from checkbox column, so do not call the methods of the controller directive. At the same time I'm not sure where force $compile in this code.
myApp.directive('myApp', ['$compile', function ($compile) {
var directive = {
restrict: 'A',
replace: true,
template: '<div></div>',
scope: {
gridConfiguration: '='
},
controller: function ($scope) {
$scope.gridIds = [];
$scope.gridIdsSelected = [];
var updateSelected = function (action, id) {
if (action === 'add' && $scope.gridIdsSelected.indexOf(id) === -1) {
$scope.gridIdsSelected.push(id);
}
if (action === 'remove' && $scope.gridIdsSelected.indexOf(id) !== -1) {
$scope.gridIdsSelected.splice($scope.gridIdsSelected.indexOf(id), 1);
}
};
$scope.updateSelection = function ($event, id) {
var checkbox = $event.target;
var action = (checkbox.checked ? 'add' : 'remove');
updateSelected(action, id);
};
$scope.isSelected = function (id) {
return $scope.gridIdsSelected.indexOf(id) >= 0;
};
$scope.selectAll = function ($event) {
var checkbox = $event.target;
var action = (checkbox.checked ? 'add' : 'remove');
for (var i = 0; i < $scope.gridIds.length; i++) {
var id = $scope.gridIds[i];
updateSelected(action, id);
}
};
},
link: function ($scope, $element, $attrs) {
var baseColumns = [
{
headerTemplate: '<input type="checkbox" id="selectAll" ng-click="selectAll($event)" ng-checked="isSelectedAll()">',
template: '<input type="checkbox" name="selected" ng-checked="isSelected(#=Id#)" ng-click="updateSelection($event, #=Id#)">',
width: 28
}
];
for (var i = 0; i < $scope.gridConfiguration.columns.length; i++) {
var column = $scope.gridConfiguration.columns[i];
baseColumns.push(column);
}
var gridOptions = {...};
var grid = $element.kendoGrid(gridOptions).data("kendoGrid");;
$scope.$parent[$attrs[directive.name]] = grid;
}
};
return directive;
}]);
I've put an example directive here: http://embed.plnkr.co/fQhNUGHJ3iAYiWTGI9mn/preview
It activates on the my-grid attribute and inserts a checkbox column. The checkbox is bound to the selected property of each item (note that it's an Angular template and it uses dataItem to refer to the current item). To figure out the selected items you can do something like:
var selection = $scope.grid.dataSource.data().filter(function(item){
return item.selected;
});
The checkbox that is added in the header will toggle the selection.
HTH.
#rGiosa is right
I've tried to do that :
var options = angular.extend({}, $scope.$eval($attrs.kOptions));
options['resizable']= true;
Seems to add the attribute in options object but the grid is still not resizable why?
http://plnkr.co/edit/Lc9vGKPfD8EkDem1IP9V?p=preview
EDIT:
Apparently,Options of the Grid cannot be changed dynamically. You need to re-create the whole Grid with different options in order to disable/enable them dynamically?!
Cheers

AngularJS - same timer for all directive instances

What is a best "Angular Way" to implement the directive that will have a shared timer for all it instances?
For example I have a directive "myComponent" and on the page it appears many times.
Inside of the component, exists some text that blink with some interval.
Because of business requirements and performance considerations, I would like that there will be single "timeout" that will toggle the blink for all instances at once (after document is ready).
I thought about the writing some code within directive definition:
//Pseudo code
angular.module("app",[]).directive("myComponent", function($timeout){
$(function() { $timeout(function(){ $(".blink").toggle(); }, 3000); } );
return {
//Directive definition
};
});
Or by using some kind of service that will receive the $element and add remove class to it:
//Pseudo code
angular.module("app",[])
.service("myService", function($timeout){
var elements = [];
this.addForBlink = function(element) { elements.push(element) };
$(function() { $timeout(function(){ $(elements).toggle(); }, 3000); } );
})
.directive("myComponent", function(myService){
return {
compile:function($element){
myService.addForBlink($element);
return function() {
//link function
}
}
};
});
In my opinion the most elegant and efficient would be to combine both these approaches by specifying the logic of the directive in the very directive initialization function. Here is a scaffold of what I actually mean:
app.directive('blinking', function($timeout){
var blinkingElements = [];
var showAll = function() {
for(var i = 0; i < blinkingElements.length; i++){
blinkingElements[i].addClass("blinking");
}
};
var hideAll = function() {
for(var i = 0; i < blinkingElements.length; i++){
blinkingElements[i].removeClass("blinking");
}
};
var blink = function () {
$timeout(showAll, 500);
$timeout(function(){
hideAll();
if (blinkingElements.length > 0) {
blink();
}
}, 1000);
};
return {
link : function(scope, element, attrs){
blinkingElements.push(element);
if (blinkingElements.length == 1) {
blink();
}
element.on("$destroy", function(){
var index = blinkingElements.indexOf(element);
blinkingElements.splice(index, 1);
});
}
}
});
And here is the working demo.
Moreover you can inject some service that will be responsible for configuration (setting the intervals and / or class) or you can provide the configuration by passing an object directly to the attribute. In the latter case you can enable applying different classes for different elements, but you should think of some policy how to deal with situation, when the interval was set more than once.

Categories

Resources