Two way binding of form input directive with isolated scope - javascript

I am leaning AngularJS, specifically working on learning directives. Would like to have a form input directive that I can re-use on all my forms to encapsulate all the boiler plate markup. Though I am having trouble getting two way binding to work in my directive. It is using an isolated scope with its own internal property to store the value of the input field. I've setup a watch on that internal property that is correctly pushing the value from within the isolate scope up to the controllers scope. What I am trying to figure out is how to take an initial value from the controllers scope and set it as the initial value in my directive.
Plunker Link: http://embed.plnkr.co/TbVB0q9DHhBCVLQ4U64W/script.js
Typing in the first input box changes the controller scopes property, but not the directives value. Typing in the second input box changes the directive and the controller property.
I know this is possible using an attribute to pass the initial value. However I am hoping to be able to extract the value from the controller scopes property via the ngModel reference in my directive.
EDIT AFTER ANSWER:
For those not sure about why you want to go through the effort of learning directives. Especially when Angular is so powerful even without using directive. This is one good reason why.
Input fields on my form without directive:
<div class="form-group">
<label for="firstName" class="col-sm-6 col-md-4 control-label">First Name</label>
<div class="col-sm-6 col-md-8" ng-class="{'has-error': userForm.firstName.$invalid}">
<input type="text" id="firstName" name="firstName" placeholder="First Name" ng-model="muState.currentUser.firstName" class="form-control" required
popover="Cannot be blank" popover-trigger="{{{true: 'mouseenter', false: 'never'}[userForm.firstName.$invalid]}}" />
</div>
</div>
After using my directive:
<ws-form-input input-name="firstName" input-label="First Name" input-placeholder="First Name"
ng-model="muState.currentUser.firstName"
required input-error="Cannot be blank"></ws-form-input>
Go through the effort. You'll be trading the headaches of learning versus a maintenance nightmare later.

I think you can simplify your directive altogether by using the isolated scope's '=' attribute notation.
Something like this:
JAVASCRIPT
app.directive('inputDirective',function(){
return {
restrict: 'E',
replace: true,
scope: {ngModel: '='},
templateUrl: "directiveTemplate.html",
require: '^form',
link: function(scope, elem, attrs, ctrl){
scope.form = ctrl;
scope.required = false;
// If attribute required exists
if (attrs.required !== undefined) {
// ng-required takes a boolean which is read from this scope variable
scope.required = true;
}
}
};
});
HTML DIRECTIVE
<div>
<input type="text" id="directiveInput"
ng-model="ngModel" class="form-control" ng-required="required"/>
<br/>
Isolated Scope value of the input box: {{ngModel}}
</div>
See UPDATED PLUNKER

You have to implement ngModel.$render, see ref here.
The following code in your directive will suffice:
scope.ngModel.$render = function() {
scope.inputBinding = scope.ngModel.$viewValue;
};

Related

angularjs directive required attribute for inner control

I am struggling with the following problem. We have a directive called deeplink. It has the following code:
restrict: 'E',
require: 'ngModel',
scope: {
smDropdown: '=smDeeplinkDropdown',
settings: '&smDropdownSettings',
onRefresh: '&smOnRefresh',
onModelChange: "&?smOnChange",
disable: "=?",
valueRequired: "=?",
hideNew: "=?",
excludeValue : "=?"
},
templateUrl: 'app/templates/smDeeplinkDropdown',
And the template for the directive is the following:
<div class="row input-group">
<select name="deeplinkDropdown"
class="form-control dropdown deeplinkDropdown"
ng-disabled="disable"
ng-required="valueRequired"
data-ng-model="dropdownModel.key"
data-ng-options="item.key as item.text disable when item.hidden for item in itemList | filter:ngOptionFilter"
data-ng-change="modelChanged(dropdownModel.key)">
<option value="">{{noneSelectedLabel}}</option>
</select>
I only show the relevant info here, not the whole HTML or whole code. Anyway, I'm struggling with the required attribute for that select. In my form I have the following:
<div data-sm-deeplink-dropdown="crud.metaData.vendors"
data-ng-model="crud.model.vendorId"
value-required="true"
name="vendorId"
id="vendorId"
data-sm-dropdown-settings="crud.getVendorSettings()"
data-sm-on-refresh="crud.refreshMetaData()">
</div>
<label class="field-validation-error control-label-error animate-show"
ng-show="form.editPurchaseOrdersGeneralForm.vendorId.$error.required">
#string.Format(Messages.isRequired, Labels.vendor)
</label>
My problem is that the select control doesn't propagate its required error back to the main directive element. So, I can not really see my validation error.
While I was writing this I thought I can probably add an ng-form either in my form or inside the directive to solve this problem. I don't really like adding too many ng-forms but I see no other solution.
What do you think?
Ok, in my form I simply changed div to ng-form and this worked (except for original form loading - it only shows in red with my error when I change to something and then to empty option)

AngularJS Directive Isolated scope inside nested ng-transclude

I've been looking all over the internet for something like this and I still can't find the answer.
I have a directive that is reused throughout my application, it allows the user to sort and search through lists of items. I have multiple kinds of items that can be passed in to the directive (html templates that I pass in) as well as multiple uses for those templates. I.e, sometimes I want a certain button on that template, but sometimes I want another. (This will make more sense in a minute).
Therefore I have created multiple directives with transclusion in order to achieve this. However, I'm having serious issues with scoping and I can't seem to figure out exactly how to pass the isolated scope to the child directive.
Below is my code:
Item List Directive
var app = angular.module('main');
app.directive('itemList', function(){
var linkFunction = function (scope, element, attributes, ctrl, transclude) {
//Things that modify the scope here. This scope is what I want to pass down to the child directives
//NOTE: I do not currently have a working transclude function which is why I didn't include it here because I have no idea what to do with it
scope.pagedItems = groupItemsToPages(items);
scope.currentPage = 0;
};
return {
restrict: 'E',
replace: 'true',
transclude: true,
templateUrl: 'partials/directives/item-list.html',
link: linkFunction,
scope: {
searchPlaceholder: "#",
items: "=",
control: "="
}
};
});
item-list.html
<div class="form-group">
<!-- I won't put all of the html here, just some to show you what i'm going for -->
<div class="search-field">
<input type="text" ng-model="query.value" placeholder="{{searchPlaceholder}}/>
</div>
<table class="table table-hover">
<tbody>
<tr ng-repeat="item in pagedItems[currentPage]">
<td ng-transclude></td>
</tr>
</tbody>
</table>
</div>
Here's the directive that simply returns the URL of whatever template is passed to it. This is so that I can add in an extra html through further nested transclusions.
item-template.js
var app = angular.module('main');
app.directive('itemTemplate', function() {
return {
restrict: 'AE',
replace: 'true',
transclude: true,
templateUrl: function(tElement, tAttrs){
return tAttrs.templateUrl;
}
};
});
Here's an example template (extremely simplified again, just to show you the layout)
profile-template.html
<div>
<p>item.name</p>
<p>item.description</p>
</div>
<div ng-transclude></div>
Here's an example of the HTML that calls this code
tab.html
<div class="tab">
<div class="available-items">
<item-list control="profileControl" search-placeholder="Search Profiles" items="profileControl.profiles">
<item-template template-url="partials/profile-template.html">
<button type="button" ng-click="launchProfile(item.id)">Launch Profile</button>
</item-template>
</item-list>
</div>
</div>
So now that you've seen the code. The issue I'm having is that my profile-template.html inclusion isn't getting the scope from the directive above it even though I've tried cloning the scope to it. All the examples I've seen require you to remove the template key from the directive and assume you're only returning the code you have in your transclusion. In my case, I have other html that I want to display in the template.
Any help would be appreciated
Instead of trying to pass the scope between your directives, you can make use of the $parent attribute and get access to higher scopes.
For instance, in your item-template controller you could gain access to the higher scope from item-list with a code like this:
if ($parent.searchPlaceholder === '') {
...
}

Dynamically add, compile and link ng-class attribute from another directive

In order give feedback on the validity in my input forms I'm using ng-class. These statements will look something like:
<div ng-class="{ 'has-error': !frmSomeName.vcHeader.$valid && frmSomeName.vcHeader.$dirty }">
<input type="text" name="vcHeader" ng-model="model.someText" ng-minlength="10" required />
</div>
I dislike the lengthiness of the statement, and would like to replace it with something alike:
<div validation-state="frmSomeName.vcHeader">
<input type="text" name="vcHeader" ng-model="model.someText" ng-minlength="10" required />
</div>
In order to avoid having to duplicate ngClass' behavior I'd like the the directive to add the ng-class directive.
This plnkr demonstrates my attempt at adding the attribute, and although it works in the simplest scenario, it is faulty and will not function with transclusion (or other more complex directives).
I know it doesn't work because of the misuse of the compile and link stages, however I'm not sure on how to actually make it work properly. Therefore my question: How do I add different directive-attribute from a directive-attribute?
The elements within the transclude directive aren't receiving the same scope. If you use the angualr $compile method and apply the scope of the transclude directive to the scope of the child directives it should work. The following should be added to your simpleTransclude directive:
link: function(scope, element) {
$compile( element.contents() )( scope )
}
remember to pass $compile into the directive.
I've forked your plnkr and applied the simpleTransclude scope to it's contents.

Add angularjs directive to a native html element (input text, div, span etc)?

I have something like this -
<input type="text" value="" title="Title"> ...
How can I add my custom directive as attribute to the existing html element on the page, let's say - directive as categoryLookup -
<input type="text" value="" title="Title" category-lookup> ...
I need to do this dynamically on page load, i.e make the input text behave per directive logic.
Thanks in advance.
You can do this with leveraging $compile after targeting your element. I added an id and used vanilla JS in this example, but you may have more at your disposal e.g. jQuery. After you have your element, just $compile it in your associated controller. Observe the following example...
<div ng-app="app" ng-controller="ctrl">
<input id="myInput" type="text" title="Title">
</div>
angular.module('app', [])
.controller('ctrl', function($scope, $compile) {
$compile(angular.element(document.getElementById('myInput')).attr('category-lookup', 'category-lookup'))($scope)
})
.directive('categoryLookup', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
console.log('directive registered')
}
}
});
JSFiddle Link - demo
Also, "dynamically on page load" can mean a few things. I assumed you are in a fully fleshed out AngularJS ecosystem, but there does exist the notion that you do not even have a controller and need to somehow cherrypick this directive and compile it on "load". Here is a nitty gritty example how you can do so, though, it's generally regarded as a bad practice, and instead leveraging the above logic in an associated controller is preferable. Observe the following...
<div ng-app="app">
<input id="myInput" type="text" value="" title="Title">
</div>
angular.element(document).ready(function () {
var $injector = angular.injector(['ng', 'app']);
$injector.invoke(function($rootScope, $compile) {
$compile(angular.element(document.getElementById('myInput')).attr('category-lookup', 'category-lookup'))($rootScope)
});
});
JSFiddle Link - demo - no controller

Access $error in Angular directive

I have an input wrapped in a directive with a dynamic name like this:
<input class="input-field" name="{{name}}" type="number" ... />
Now I want to access the $error variable of the form IN the directive.
Something like form.{{name}}.$error.number.
Is there a way to do this?
If you want to access the form (that is on the parent scope) you have to pass the form to your directive. To do this, you have to specify that you want two way binding (using =) when you define your directive.
Have a look at https://docs.angularjs.org/guide/directive, more specifically the part about isolating the scope can probably help you.
Maybe it's enough to get the $error of the specific ngModelController?
return {
template: '<input ng-model="value" type="number" /><span ng-bind="error | json"></span>',
scope : {},
link: function(scope, elem, attr) {
scope.error = elem.find('input').controller('ngModel').$error;
}
}
http://plnkr.co/edit/wzuWT1lVevCLHkLLBIAT?p=preview

Categories