AngularJS: how to compile form in a directive - angularjs

I'm trying to create a custom directive that set 'required' and 'disabled' attributes to the element (form input). The directive gets this data from the scope object. But clearing of required field doesn't set my form to invalide state. I think it's about form compilation after changing the attribute. I tried to do that but got an infinite loop :(
How to compile my form correctly?
Here is plunker

You could just use ng-disabled and ng-required, instead of adding the attributes in a directive.
<div>
<label for="username">username2</label>
<input ng-model="data.account.username2"
ng-disabled="paintData['data.account.username2'] == 'RO'"
ng-required="paintData['data.account.username2'] == 'R'" />
</div>
Alternatively, you could define a directive template that uses ng-disabled and ng-required, and replace the original element with it:
.directive('metaValidate', function($compile) {
return {
restrict: 'A',
replace: true,
scope: {
model: '=',
paint: '='
},
template: '<input ng-model="model" ng-disabled="readonly" ng-required="required"/>',
link: function(scope, element, attrs) {
scope.required = scope.paint[element.attr('model')] === 'R';
scope.readonly = scope.paint[element.attr('model')] === 'RO';
}
};
});
Then, use it like this:
<input model="data.account.username2" meta-validate paint="paintData"/>
I prefer the first approach, because it can respond to changes to paintData. But, you have to repeat the property name several times.
If you want to try this code, here is an update of your Plunker.

Recompiling the element could work, but in order to avoid the infinite loop you need to first remove the meta-validate attribute (which would cause yet more compiling etc):
/* At the end of the directive's link function */
attrs.$set('metaValidate', undefined);
$compile(element)(scope);
See, also, this short demo.

Related

Using angular scope directive variable content as an attribute itself

I couldn't find the answer to this question anywhere, so I'm gonna ask it here. Is there any way to use an Angular scope directive variable content as an attribute itself?
For example:
View Input:
<custom-directive
attr-one="Atribute value 1"
ng-model="cool.model"
message="Message 1"
extra-attr="variable-attribute"
></custom-directive>
Directive file:
app.directive('customDirective', [
function () {
return {
restrict: 'E',
templateUrl: createuri('/templates/custom-directive'),
require: 'ngModel',
scope: {
message: '#message',
ngModel: '=ngModel',
extraAttr: '#extraAttr',
attrOne '#attrOne'
}
}
}
]);
Directive template file:
<input type="text"
attr-one="attrOne"
class="input-directive"
ng-model="ngModel"
message="message"
{{extraAttr}} %{--something like this--}%
/>
In a way that the output would end up like this:
<input type="text"
attr-one="Atribute value 1"
class="input-directive"
ng-model="cool.model"
message="Message 1"
variable-attribute
/>
Edit: I'm not sure it's a assignment error, because when I try to use a variable that is working ({{label}}), for eg., this is what i get:
The variable gets outputed inside the element's content area, but not inside the element attribute definition area.
As said in here:
"Web browsers are sometimes picky about what values they consider valid for attributes."
Try to use ngAttr for this:
ng-attr-label="{{yourLabelValue}}"
label can be replaced with any attribute name such as ng-attr-variable-attribute="{{attributeValue}}" for "variable-attribute".
I end up finding the answer, and it consists on using the compile directive function as It runs before the link one.
/*[...]*/
priority: 1001,
terminal: true,
compile: function(el, attr) {
var ipt = el[0].childNodes[0].childNodes[1];
/* ^ searching for the element I want to bind the directive attribute to*/
ipt.setAttribute(attr.extraAttr, '');
/* ^ setting the attribute to the value contained in extraAttr */
}
/*[...]*/

HTML parsed through $compile, ng-model not binding in isolation for ng-repeat

I am building an Angular module that will allow a form to be built dynamically.
As elements are selected, HTML is added to a model. The model is attached to an ng-repeat element.
<div ng-repeat="item in list1 track by $index">
<div compiledom="item.content"></div>
</div>
So an item in the model might look like this:
{
'title': 'Full-Column Text',
'drag': true,
'inputValue': '',
'content': '<div class="small-12 columns"><input type="text" dynamic-model="$index" placeholder="Full Width Text" /></div>'
}
I am using a custom directive to compile the HTML fed to the model.
.directive('compiledom', function($compile) {
return function(scope, element, attrs) {
scope.$watch(
function(scope) {
return scope.$eval(attrs.compiledom);
},
function(value) {
element.html(value);
$compile(element.contents())(scope);
}
);
}
})
And using a second directive to bind the model data to the input field in that HTML.
.directive('dynamicModel', function($compile) {
return function(scope, element, attrs) {
scope.$watch(attrs.dynamicModel, function(dynamicModel) {
if (attrs.ngModel || attrs.ngModel == dynamicModel || !dynamicModel) return;
element.attr('ng-model', 'item.inputValue'); <---------- bound here
if (dynamicModel == '') element.removeAttr('ng-model');
element.unbind();
$compile(element)(scope);
});
}
})
My issue is that whatever I put into an input field gets placed to every input element. For some reason, it appears that a single item.inputValue is getting reflected to every item of the same type. The model is bound, but I have broken something in ng-repeat that keeps it in isolation.
For example, if I have two 'Full-Column Text' inputs, if one is set to 'ABC', both are set to 'ABC'. If I also were to have 2 'Half-Column Text' inputs, they would remain unset until I set one of them to 'DCE' - then they are both set to 'DCE'.
A link to a demo/example of the issue will be shared soon.
As it turned out, my directives were fine.
When I was adding to my model, I was using .slice. This was causing a reflection issue. Using angular.copy made a geniune clone, allowing the isolation I was looking for.
$scope.list1[x] = angular.copy($scope.list5[x]);

Passing a model to a custom directive - clearing a text input

What I'm trying to achieve is relatively simple, but I've been going round in circles with this for too long, and now it's time to seek help.
Basically, I have created a directive that is comprised of a text input and a link to clear it.
I pass in the id via an attribute which works in fine, but I cannot seem to work out how to pass the model in to clear it when the reset link is clicked.
Here is what I have so far:
In my view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', attrs.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope[attrs.inputModel] = '';
});
}
};
});
I'm obviously missing something fundamental - any help would be greatly appreciated.
You should call scope.$apply() after resetting inputModel in your function where you reset the value.
elem.find('a').bind('click', function() {
scope.inputModel = '';
scope.$apply();
});
Please, read about scope in AngularJS here.
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
I've also added declaring of your inputModel attribute in scope of your directive.
scope: {
inputModel: "="
}
See demo on plunker.
But if you can use ng-click in your template - use it, it's much better.
OK, I seem to have fixed it by making use of the directive scope and using ng-click in the template:
My view:
<text-input-with-reset input-id="the-relevant-id" input-model="the.relevant.model"/>
My directive:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
scope: {
inputModel: '='
},
template: '<div class="text-input-with-reset">' +
'<input ng-model="inputModel" id="input-id" type="text" class="form-control">' +
'<a href ng-click="inputModel = \'\'" class="btn-reset"><span aria-hidden="true">×</span></a>' +
'</div>',
link: function(scope, elem, attrs) {
elem.find('input').attr('id', attrs.inputId);
};
});
It looks like you've already answered your question, but I'll leave my answer here for further explanations in case someone else lands on the same problem.
In its current state, there are two things wrong with your directive:
The click handler will trigger outside of Angular's digest cycle. Basically, even if you manage to clear the model's value, Angular won't know about it. You can wrap your logic in a scope.$apply() call to fix this, but it's not the correct solution in this case - keep reading.
Accessing the scope via scope[attrs.inputModel] would evaluate to something like scope['the.relevant.model']. Obviously, the name of your model is not literally the.relevant.model, as the dots typically imply nesting instead of being a literal part of the name. You need a different way of referencing the model.
You should use an isolate scope (see here and here) for a directive like this. Basically, you'd modify your directive to look like this:
app.directive('textInputWithReset', function() {
return {
restrict: 'AE',
replace: 'true',
template: [...],
// define an isolate scope for the directive, passing in these scope variables
scope: {
// scope.inputId = input-id attribute on directive
inputId: '=inputId',
// scope.inputModel = input-model attribute on directive
inputModel: '=inputModel'
},
link: function(scope, elem, attrs) {
// set ID of input for clickable labels (works)
elem.find('input').attr('id', scope.inputId);
// Reset model and clear text field (not working)
elem.find('a').bind('click', function() {
scope.inputModel = '';
});
}
};
});
Notice that when you define an isolate scope, the directive gets its own scope with the requested variables. This means that you can simply use scope.inputId and scope.inputModel within the directive, instead of trying to reference them in a roundabout way.
This is untested, but it should pretty much work (you'll need to use the scope.$apply() fix I mentioned before). You might want to test the inputId binding, as you might need to pass it a literal string now (e.g. put 'input-id' in the attribute to specify that it is a literal string, instead of input-id which would imply there is an input-id variable in the scope).
After you get your directive to work, let's try to make it work even more in "the Angular way." Now that you have an isolate scope in your directive, there is no need to implement custom logic in the link function. Whenever your link function has a .click() or a .attr(), there is probably a better way of writing it.
In this case, you can simplify your directive by using more built-in Angular logic instead of manually modifying the DOM in the link() function:
<div class="text-input-with-reset">
<input ng-model="inputModel" id="{{ inputId }}" type="text" class="form-control">
<span aria-hidden="true">×</span>
</div>
Now, all your link() function (or, better yet, your directive's controller) needs to do is define a reset() function on the scope. Everything else will automatically just work!

Angularjs - ng-disabled default value

I have a button with this attribute: ng-disabled="ProductionBtnDisabled".
When angular renders the html the value of ProductionBtnDisabled is undefined and after initiating the controller ProductionBtnDisabled has the correct value.
So, at first the ng-disabled is not disabled because undefined=false in javascript/angular. This is a problem for me. I want the default value to be true.
Is any one has any suggestion to handle this?
What about using ng-cloak? It didn't work for me. I don't mind hiding the buttons until the scope is rendered.
Thanks!
I think there is no default settings for ngDisabled. If you needs a global solution, you can try directive.
.directive('specialDisabled',function() {
return {
restrict: 'A',
scope:{specialDisabled: '='},
link: function(scope, element, attrs) {
scope.$watch(function(){return scope.specialDisabled}, function(){
// set disabled attribute here
element[0].disabled = !scope.specialDisabled;
});
}
}
});
Then, you can called specialDisabled directive anywhere.
<input type="checkbox" ng-model="chk">
<button special-disabled="chk">My Button</button>
http://plnkr.co/edit/BA2ntTrzmItwvEj8UKOc?p=preview

Issues with ng-required directive angular js

I want my input to be required based on condition.
<input type="text" custom-directive ng-required="isRequired" />
I have defined property isRequired as boolean false. Required functionality works fine according to the isRequired property. But inside the custom directive i am getting $attrs.required as true for all scenarios.
module.directive('customDirective', function () {
var link = function ($scope, $el, $attrs, $controllers) {
$scope.required = $attrs.required;
}
return {
restrict: 'A',
link: link
};
});
Thanks
I am unable to explain why $scope.required would be true in this case (perhaps you are setting it to true somewhere else and should be using a child or isolation scope?)... I would expect it to be undefined based on what you have provided. The other possibility is that you have multiple elements with this directive in the same scope and one of them is setting that value to true for all of them.
By replacing your $scope.required = $attrs.required; line with
$attrs.$observe('required', function(value) {
$scope.required = value;
});
it should properly pick up the value of the required attribute (and when it changes!).
If you have that directive being applied to multiple elements in the same scope you should use a child scope. All you need to do is add scope: true to the object returned by your directive.
If this does not work, please post more of your application's code.

Resources