I am trying to create N number of Select controls dynamically from directive based on array that is passed in from the attribute (where N is the length of the array).
Structure of an object of the array is as such:
selectDescription = {
array: arrayObject, //ng-options, a string as 'item as item.name for item in selectArray[0]'
change: methodName, //ng-change, actionname
level: levelNumber //level number
}
So the number of select controls inside span tag depends on the number of selectDescription(s) that I get from the attribute.
First select control is rendered successfully. Subsequent select controls should have been rendered on select of an option from previous rendered select controls. But it's not happening in my case. Although I am successfully appending angular elements in the current inputEl(on select of an option), it is not being rendered in the UI. I guess I am missing something very crucial.
On change of selectDescriptions, a flipped attribute is set, through which I am able to call scope.$editable.render() from link, which in turn runs render function to re-append elements after clearing the previous HTML inside span.
My Code:
app.directive('editableLocation', function(editableDirectiveFactory) {
var createElement = function(el, index){
var newElement = angular.element("<select/>");
newElement.attr('ng-model','$data'+index);
newElement.attr('ng-options',el.array);
newElement.attr('ng-change',el.change.substring(0, el.change.length - 1)+", $data"+index+")");
return newElement;
}
var descriptions = [] ;
var dir = editableDirectiveFactory({
directiveName: 'editableLocation',
inputTpl: '<span></span>',
render: function() {
this.parent.render.call(this);
this.inputEl.html("");
for(var i = 0 ; i < descriptions.length ; i ++){
this.inputEl.append(createElement(descriptions[i], i));
}
}
});
var linkOrg = dir.link;
dir.link = function(scope, el, attrs, ctrl) {
console.log(el);
descriptions = scope.$eval(attrs.description);
scope.$watch('flipped',function(newValue,oldValue){
if(newValue != 0){
scope.$editable.render();
}
});
return linkOrg(scope, el, attrs, ctrl);
};
return dir;
});
Since you are adding the dynamic HTML content in the link function of the Angular directive, Angular will not auto compile/parse it. You need to do it manually using $compile directive. So after you appended all the HTML, do the following (inject $compile in your code)
$compile(element.contents())(scope);
Where element is your any parent element where you are generating dynamic HTML and scope is the scope of the directive or any other scope which you want it to be attached to the dynamic HTML.
Looking at xeditable.js I have found that xeditable renders the UI by calling a show method defined in its editableController.
It is defined as:
self.show = function() {
self.setLocalValue();
self.render(); //calls 'render' function of 'editableDirectiveFactory'; that' where my custom UI lies
$element.after(self.editorEl); //attaches newelement(especially whole <form/> element)
$compile(self.editorEl)($scope); //renders whole UI(and also the newly attached one)
self.addListeners();
$element.addClass('editable-hide');
return self.onshow();
};
So what I felt is, I need to call this show method from my link function, which receives the controller.
This is what I did:
dir.link = function (scope, el, attrs, ctrl) {
$element = el;
scope.$watch(attrs.flipped, function (newValue, oldValue) {
//re-render element if flipped is changed; denoting description of select controls have been altered
if (newValue != 0) {
ctrl[0].show(); //this will call render function and also $compile({{content/html/element}})(scope)
}
});
return linkOrg(scope, el, attrs, ctrl);
};
And also you need to hide the previous <form/> element(which contains previous rendered UI), so that only one forms get displayed.
This is how I hid that previous <form/> element in render' function ofeditableDirectiveFactory`:
var prevForm = $element[0].nextElementSibling; //hide previous form element which would already contain previous select
if (prevForm)
prevForm.classList.add('editable-hide');
That solved my problem at least :)
Related
Here's the scenario.
In the app, you can add inline custom code (HTML attributes ex. style="", onclick="alert('Test')") in an element (ex. input texts, divs). The custom code is binded to the main model and loaded to the element using a custom directive I've created. I'm doing this to control dynamically generated fields that I want to hide and show based on different inputs.
This is my custom directive that loads inline attributes on the element:
app.directive('addCustomHtml', function() {
return {
scope: {
customHtml: "="
},
link: function(scope, element, attributes){
scope.$watch('customHtml', function(newVal, oldVal) {
if (newVal) {
var attrs = newVal.split('\n');
for (var i = 0; i < attrs.length; i++) {
var result = attrs[i].split('=');
var attr = result.splice(0,1);
attr.push(result.join('='));
if (attr[1]) {
element.attr(attr[0], attr[1].replace(/^"(.*)"$/, '$1'));
}
}
} else {
if (oldVal) {
var attrs = oldVal.split('\n');
for (var i = 0; i < attrs.length; i++) {
var attr = attrs[i].split('=');
if (attr[0]) {
element.removeAttr(attr[0]);
}
}
}
}
})
}
}
});
It is binded to the element like this:
<input type="checkbox" add-custom-html custom-html="checkbox1.customHtml">Yes
To see it in action, you can check the plunkr here: https://plnkr.co/edit/xjjMRPY3aE8IVLIeRZMp?p=preview
Now my problem is, when I try to add AngularJS directives (ex. ng-show, ng-if) using my custom directive, AngularJS doesn't seem to recognize them and the model scope I'm passing inside.
Another problem is when I try to add vanilla Javascript event functions (ex. onclick="", onchange=""), it does work but sometimes AngularJS does not read them especially when the element has an ng-change, ng-click attributes.
Again, I am doing this approach on the app because I have generic fields and I want to control some of them by adding this so called "custom codes".
Any help would be highly appreciated!!
If you want to add HTML code and compile it within current $scope, you should use the $compile service:
let someVar = $compile(yourHTML)($scope);
// you can now append someVar to any element and
// angular specific markup will work as expected
Being a service, you'll need to inject it into current controller (or pre/post link function) to be able to use it.
I have an angularjs dropdown directive called X that opens and shows related items when I start typing on an input field. There are two X directives in my page which have the same controller and template (ie same directive but different isolated scope).
The first directive shows list of planets and the second shows list of minerals in the planet that is being typed. When I start typing on an input, I want to open both directive showing related data dynamically. But as they share same template, I don't know how to open the dropdown for the second directive.
There is a single ng-repeat in the template but 2 lists on 2 different scopes. I just want to know the approach in understanding this. Thank you.
Here directive will listen inpVal and updates the list in directive (you need to pass inpVal to the directive).
.directive("myCustomSelect", function(){
return {
restrict: "AE",
scope: {
inpVal: "="
},
link: function(scope, ele, attrs){
var _list = [];
scope.list = _list;
var _watch = scope.$watch("inpVal", function(n){
if(n){
updateList();
}
});
scope.$on("$destroy", function(){
_watch();
});
function updateList(){
var enteredVal = scope.inpVal;
//_.filter is underscore js
scope.list = _.filter(_list, function(v){
return enteredVal == v.name?true:false;
});
}
},
template: "<select ng-options="l.name as l for l in list"></select>"
}
})
I am trying to get the child input tag for the current checkbox that is clicked and add checked to it through my directive. I have the directive setup correctly but I am getting undefined when I try to get elem[1]
HTML:
<div class="checkbox">
<input id="checkbox1" type="checkbox" checked ng-model="checkboxoption.value1">
<span class="custom"></span>
<label for="checkbox1">Checkbox 1</label>
</div>
JS:
.directive('checkbox', [function() {
return {
restrict: 'C',
scope: {},
link: function(scope, elem, attr) {
elem.bind('click', function(evt) {
var currentCheckbox = elem[1];
console.log(elem[1]);
elem.prop('checked');
});
}
};
}])
elem is a jQuery or jqLite object. The subscript lets you index into the collection of elements matched by a selector. So for example, in jQuery,
$("span")[1]
gets the second span on the page. On the other hand,
$("body")[1]
should return undefined because there should only be one body.
There is only one element (the <div class="checkbox">) in elem. To get its second child, you can do this:
elem.children()[1]
But you probably want its first child, since the checkbox comes first in your HTML:
elem.children()[0]
Another approach is:
elem.find("input")[0]
That may be better since it won't break if you change the order of the elements in your HTML.
Both of these will get you a plain DOM object. Once you have the checkbox element, you can set its checked attribute like this:
elem.children()[0].checked = true;
// or
elem.find("input")[0].checked = true;
By the way, you should probably remove the id element from your checkbox, because if you use this directive more than once, the ID will be duplicated.
You can simply do like this using Angular#angular.element;
You can get the current checked element using jQuery#target property of event
link: function(scope, elem, attr) {
elem.bind('click', function(evt) {
angular.element(evt.target).attr('checked',true);
});
}
Here is the working Plunker
you are trying to access elem[1] which does not exist in the array elem.
Instead of using elem[1] use elem[0]
I have written a directive that sets the child elements of parent element equal to the height of the tallest child element. Here's the directive code:
app.directive('equalHeightChildren', function(){
return function(scope, element, attrs){
var $tallest = element;
$.each(element.children(), function(index, child){
if($(child).outerHeight() > $tallest.outerHeight()){
$tallest = $(child);
}
});
element.children().outerHeight($tallest.outerHeight() + 'px');
}
});
This code runs as the page loads and correctly adjust the heights. However, I have certain scope variables in my app that can change the height of these child elements (for example checking a checkbox displays a new form in one of the child elements, increasing its height). I was hoping that the directive would rerun itself when one of these variables changes, thereby re-adjusting the heights of the child elements. However, this doesn't seem to be happening.
Is there a way to have the directive run when the scope variables change? Or am I thinking about this incorrectly?
If you want it to re-run the each you might need a watcher on the element thats changing.
Another option is to a fire an event and have this directive handle it.
app.directive('equalHeightChildren', function(){
return function(scope, element, attrs){
var setTallest = function(){
var $tallest = element;
$.each(element.children(), function(index, child){
if($(child).outerHeight() > $tallest.outerHeight()){
$tallest = $(child);
}
});
element.children().outerHeight($tallest.outerHeight() + 'px');
}
setTallest();
//Use one of these to cause the height to be reset
//$scope.$watch('myVar', function() {setTallest(); });
// $rootScope.on('eventTrigger', setTallest);
}
});
I have a layout with multiple elements which are able to gain target. I need to target only one element at the time.
Is it possible to define a function on the $scope that receives an object from the model (for example a line item belonging to an invoice) and tell Angular to add a css class wherever the view of this model is?
If I use the ng-class directive, it would force me to add ng-class to all "targetable" elements in the html and each element should know if it is the current target or not. I don't want to add an isTarget() function to each possible element because it will dirty the model.
Example:
This is the html:
<p>{{document.shipFrom}}</p>
<p>{{document.shipTo}}</p>
<ul>
<li ng-repeat="item in document.items">{{item.description}}</li>
</ul>
And this is the controller:
angular.module('myApp').controller('DocumentCtrl', function($scope){
$scope.document = {
shipFrom: 'Origin',
shipTo: 'Destination',
items: [
{description:'item1'},
{description:'item2'}
]
};
})
Is there a way to define $scope.setTarget($scope.document.items[0]) so that it adds a class "on-target" to the element? Note that all the document properties (the items and the shipFrom/To) can gain target.
Edit: Solved
I found a way to get a model's attribute value in my directive's linking function. If I use the $parse service then I can evaluate the model's property attached to the directive simply by instantiating a getter function:
link: function postLink ($scope, $iElement, $iAttrs) {
var valueGetter = $parse($iAttrs.ngModel);
//Then, I can subscribe the directive to a custom event:
$scope.$on('set.target', function (event, elem) {
$iElement.removeClass('on-target alert-info');
//Now it gets the actual value of the model related to the directive
var value = valueGetter($scope);
//If both the model and the event's value are the same, then focus the element.
if (value == elem) {
$iElement.addClass('on-target alert-info');
$scope.setTarget(valueName, elem);
$scope.$apply();
}
return;
});
}//end link function
When I need something to gain target from the controller, then I just do $scope.$broadcast('set.target', $scope.document.shipFrom)
HTML Part :
<p>{{document.shipFrom}}</p>
<p>{{document.shipTo}}</p>
<ul>
<li ng-repeat="item in document.items" ng-click="setTarget(item.description)" ng-class="{'active' : selectedTarget == item.description}">{{item.description}}</li>
</ul>
Controller:
$scope.document = {
shipFrom: 'Origin',
shipTo: 'Destination',
items: [
{description:'item1'},
{description:'item2'}
]
};
$scope.selectedTarget = '';
$scope.setTarget = function(data) {
$scope.selectedTarget = data;
};
DEMO
If you don't want to add an isTarget() function to each possible item, you could add isTarget method on document.
isTarget: function(item){
return this.target === item;
}
and change the html to
<li ng-repeat="item in document.items" ng-class="{'on-target': document.isTarget(item)}">{{item.description}}</li>