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.
Related
For some reason when using this function('testclickfn') as ng-click on dynamic elements, it doesn't invoke the function. Here is the angularjs file:
app.controller('testctrl',function($scope){
testfn($scope);
$scope.showelements = function(){
displayTestRows();
}
});
function testfn($scope){
$scope.testclickfn = function(){
alert('testing click fn');
};
}
function displayTestRows(){
for(var i=0; i < 5; i++){
$("#testdiv").append('<p ng-click="testclickfn()">click me</p><br>');
}
}
HTML page that calls angularjs controller 'testctrl':
<div id="testdiv" ng-controller="testctrl">
<button ng-click="showelements()">Show dynamic elements</button><br>
</div>
I'm assuming since the 'click me' tags are being generated after angular has loaded the page, it doesn't know of anything after page is generated so ng-click="testclickfn()" doesn't get registered with angularjs.
How do I get around this situation?
You're creating elements in a way angular has no idea about (pretty bad practice), but not to worry, you can let angular know!
Change the controller signature to
controller('testctrl', function($scope, $compile) {
Then run compile the new elements manually to get the ng-click directive activated
$scope.showelements = function(){
displayTestRows();
$compile($("#testdiv").contents())($scope);
}
If you cant tell, having to use jquery selectors inside your controller is bad, you should be using a directive and the link function to attach the element to the scope (ie, what if you have multiple testctrl elements?), but this'll get you running
As promised
The general rules are that no JS should be outside the angular functions, and that DOM manipulation, where appropriate should be handled by angular also.
Example 1: powerful
Have a look
<div ng-controller="ctrl">
<button ng-click="show('#here')">
create
</button>
<div id="here">
I'll create the clickables here.
</div>
</div>
use controllers for things that share stuff between a lot of different things
.controller('ctrl', ['$scope', '$compile', function($scope, $compile) {
$scope.sharedVariable = 'I am #';
$scope.show = function(where) {
where = $(where).html('');
//lets create a new directive, and even pass it a parameter!
for (var index = 0; index < 5; ++index)
$('<div>', {'test':index}).appendTo(where);
$compile(where.contents())($scope);
};
}])
use directives for non-unique elements that each have their own states
.directive('test', function() {
return {
//these too have their own controllers in case there are things they need to share with different things -inside them-
controller : ['$scope', function($scope) {
$scope.test = function() {
//see, no selectors, the scope already knows the element!
$scope.element.text(
//remember that parent controller? Just because we're in another one doesnt mean we lost the first!
$scope.$parent.sharedVariable +
$scope.index
);
}
}],
//no need to do things by hand, specify what each of these look like
template : '<p>click me</p>',
//the whole "angular way" thing. Basically no code should be outside angular functions.
//"how do I reference anything in the DOM, then?"; that's what the `link` is for: give the controller access using `scope`!
link : function(scope, element, attributes) {
//you can assign "ng-click" here, instead of putting it in the template
//not everything in angular has to be HTML
scope.element = $(element).click(scope.test);
//did you know you can accept parameters?
scope.index = Number.parseInt(attributes.test) + 1;
},
//just some set up, I'll let you look them up
replace : true,
restrict : 'A',
scope : {}
};
})
Example 2: Simple
But that is just a very generic and powerful way of doing things. It all depends on what you need to do. If this very simple example was indeed all you needed to do you can make a very simple, almost-all-html version:
<div ng-controller="ctrl">
<button ng-click="items = [1, 2, 3, 4, 5]">
create
</button>
<p ng-repeat="item in items" ng-click="test($event)">
<span>click me</span>
<span style="display:none">I am #{{item}}</span>
</p>
</div>
.controller('ctrl', ['$scope', function($scope) {
$scope.test = function($event) {
$($event.currentTarget).children().toggle();
};
}])
That's it, works the same almost
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 :)
I'm writing a directive wrapper around a typeahead input. This directive listens for changes on a link and get's new data + options for the typeahead.
I can simply simulate this behaviour with a $timeout and demonstrated it in this plnkr.co.
JS
app.controller('sample', function($scope, $timeout) {
$scope.options = ['1800', '1900', '2100'];
// Simulate some latency
$timeout(function () {
$scope.options.push('1850');
}, 4000);
});
HTML
<div>
<input type="text" ng-model="optionValue" typeahead="opt for opt in options | filter:$viewValue">
</div>
If you start typing '18' in the input field it shows 1800 as expected. But when 1850 get's added after an amount of time, the selectable options from typeahead are not being updated.
-- FYI my real live directive looks like this --
$scope.$watch($interpolate(url), function (newUrl) {
$http.get(newUrl).then(function (response) {
$scope.options = response;
});
});
I tried to use typeahead="opt for opt in getData()" but this doesn't work because the interpolated value is not yet up to date. It's always one value behind.
Seems like an issue to post on AngularUI Bootstrap website. Matches are getting selected on every keystroke but they don't get updated if you change the underlying data between keystrokes. I don't see any work-around for this, except maybe triggering the appropriate key event handler on the input manually (when you change the collection).
If someone interested in the solution, here is how I solved it at the moment. I'm not happy with the end result, please provide me some feedback :-).
Plunkr
Check out updated-bootstrap.js, I had to add the following in order to make it work:
A custom attribute that'll be use for the $watchCollection
var customOptions = attrs.typeaheadCustomOptions || '';
In the function where it gets the matches I've added a watch if customOptions is provided:
if (customOptions) {
originalScope.$watchCollection(customOptions, function (matches) {
resetMatches();
updateMatches(matches);
});
}
And that was basically it :-), the updateMatches is just an abstraction of existing code. It's not being used by me and the manual update.
var updateMatches = function(matches) {
for (var i = 0; i < matches.length; i++) {
locals[parserResult.itemName] = matches[i];
scope.matches.push({
id: getMatchId(i),
label: parserResult.viewMapper(scope, locals),
model: matches[i]
});
}
scope.query = modelCtrl.$viewValue;
};
Opened issue on github
I am new to angularjs.....I am trying to write a directive which adds some html before and after an element...html is as desired but data binding not happening ... please help
plunker link
my precompile function is as follows
var linkFunction = function(scope,element,attrs){
element.removeAttr("cs-options");
var html = getHTML(element);
element.replaceWith(html);
$compile(element.parent())(scope);
}
Here's a way simpler solution, I'm using transclude to have the contents of the element copied into the template.
app.directive('csOptions',["$compile",function($compile){
return{
restrict:'A',
transclude:true,
template:"<form><div ng-transclude></div></form>"
}
}])
http://plnkr.co/edit/fqHr6i
The data binding does not work because the getHTML() method not copying {{abc}} along with the element. You need to update the link method as:
var linkFunction = function(scope,element,attrs){
// do not miss {{abc}}
var $parent = element.parent();
element.removeAttr("cs-options");
var html = getHTML($parent);
// override the parent not the element otherwise
// there will be two instances of {{abc}}
$parent.html(html);
$compile($parent)(scope);
}
Demo: http://plnkr.co/edit/mckBVu1HfT4fp90Twvum
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>