AngularUI bootstrap typeahead in custom directive - javascript

I have successfully used UI Bootstrap's typeahead directive on my AngularJS pages, and I would like to create a custom directive for account selection that wraps up the typeahead with some services for fetching the account options.
The options show up just fine, but I think I'm running into issues with the ng-model passed as a $scope variable from the outer container to the custom directive - it keeps cutting me off when I try to actually type out the selection in the custom directive.
When using uib-typeahead in my outer controller it works just fine and waits forever for you to finish typing/make your selection. But when doing it in the directive, the ng-model-options: { debounce: 50 } seems to cause the user's typing to reset to empty after the debounce interval. At 50 ms I basically can get only a character or two in there, and then the $viewValue (?) is reset, and it often garbles both the possible selection and the placeholder in the text box. If I set the debounce to a higher value, then it gives me longer before it resets, but it is also that much slower to react to user input.
I'm guessing the issue has something to do with the ng-model, its underlying ngModelController and the $viewValue in there, but I'm not sure how to get it to let me finish typing.
Plunker demo can be found here
WORKS: uib-typeahead
<input type="text"
name="account2"
uib-typeahead="account as formatAccount(account) for account in accounts | filter:$viewValue"
placeholder="Select an account"
ng-model="selectedAccount"
ng-model-options="{ debounce: 50 }"
typeahead-show-hint="true"
typeahead-focus-first="true"
typeahead-editable="false"
typeahead-min-length="0"
class="form-control account-search"
autocomplete="off" />
DOESN'T WORK: Reference to the directive
<input type="text"
name="account3"
input-account
accounts="accounts"
placeholder="Select your account"
ng-model="selectedAccount" />
The impatient directive
app.directive('inputAccount', [
function() {
let directive =
{
restrict: 'A',
replace: 'true',
require: ['ngModel'],
templateUrl: 'input-account.html',
scope: {
ngModel: '=ngModel',
accounts: '=',
placeholder: '#',
name: '#',
required: '#'
},
link: linkFunction
};
return directive;
function linkFunction($scope) {
$scope.formatAccount = function (account) {
if (account) {
return `${account.id} - ${account.name}`;
}
return '';
}; // formatAccount
}
}
]); // inputAccount directive
The impatient directive's template
<input type="text"
name="{{ name }}"
placeholder="{{ placeholder }}"
ng-model="ngModel"
ng-model-options="{ debounce: 50 }"
uib-typeahead="account as formatAccount(account) for account in accounts | filter:$viewValue"
typeahead-show-hint="true"
typeahead-focus-first="true"
typeahead-editable="false"
typeahead-min-length="0"
class="form-control account-search"
autocomplete="off"
ng-required="required" />

Related

Using angular form in directive template

I am using require: '^form' in my simple directive.
Then I trying to use this form in an ng-show but it doesn't seem to work.
Note: I don't want to pass the form name in as an attribute.
Can anyone see where i am going wrong? I only want the message to show when the form is invalid.
angular.module('xxx').directive('errorWall', errorWall);
function errorWall() {
return {
restrict: 'E',
require: '^form',
scope: {},
link: (scope, elm, attrs, frm) => {
scope.formCtrl = frm;
},
template: '<div ng-show="formCtrl.$invalid">You have error messages.</div>'
};
}
Make sure you've placed the directive inside the form with at least one input with a ng-model directive on it.
<form>
<input type="text" ng-model="name" required />
<error-wall></error-wall>
</form>
Here's a working fiddle https://jsfiddle.net/3gv8nvL3/3/ with one form required input.

How can I add an element within the directive?

I have an input box that I am trying to add functionality to. I want to write a class directive to easily add an ability to any input box. I want that when the user enters an '#', a box will pop up showing the options, using uib-typeahead. I cant figure out a way to display the box without making the original text box disappear, although ideally the box will pop up where the user is currently typing
<div class="medium-2">
<label>Age:</label>
</div>
<div class="medium-10">
<input type="text" class="form-control pop-up-variables" name="age" error-message="ageErrorMessage" required>
</input>
</div>
The block I want to be able to add is this:
<input class="variable-dropdown" type="text" uib-typeahead="number as number for number in numbers | filter:$viewValue" ng-model="selectedAttribute" placeholder="Choose variable">
</input>
I don't want to return it as a template in the directive because I dont want to replace the block that the directive is on, but I don't know how to add it correctly to the DOM.
The simple JS looks like:
app.directive('popUpVariables', function() {
return {
restrict: 'C',
controller: function($scope) {
$scope.numbers = ['One', 'Two', 'Three', 'Four', 'Five'];
},
link: function (scope, element, attrs, ngModelCtrl) {
element.on('keypress', function(event) {
if (event.which === 64) {
// This is where I want to show the second input
}
});
}
}
})

Angular directive with two input elements: can't get $watch to work on ng-model value

I'm trying to write an Angular directive which has a template containing two input fields (eventually this will be a Date and Time picker) and can't get $watch to fire on either of the input value changes. I have as a test:
HTML
<tw-input-test label="Test" ng-model="myModel.value"></tw-input-test>
Angular
app.directive("twInputTest", function(){
return{
restrict: 'E',
require: 'ngModel',
scope:{ label:'=' },
templateUrl:"templates/tw-input-test.html",
link: function(scope,element,attrs){
scope.twone=42;
scope.twtwo=27;
scope.updateTest=function(){
scope.label= scope.twone.toString()+"-"+scope.twtwo.toString();
}
scope.$watch(scope.twone,scope.updateTest, true);
scope.$watch('scope.twtwo',scope.updateTest, true);
}
};
});
Template
<div class="form-item">
<span>{{label}}</span>
<input type='number' style="width:80px" ng-model="twone">
<input type='number' style="width:80px" ng-model="twtwo">
</div>
I can see the values of scope.twone, scope.twtwo change when the input boxes are altered (using Chrome dev tools), but updateTest() is only called when the link function is initially called.

Validate a field from a directive in AngularJS

I am trying to wrap an input and some markup into an AngularJS directive.
However the fields are supposedly always validated which they shouldn't be.
Please check out my example # http://plnkr.co/edit/TivmuqQI4Y5K56gwcadW
here is the code for those who do not wish to look at Plunker
my directive
app.directive('myInput', function() {
return {
restrict: 'E',
require: '^ngModel',
templateUrl: 'form_control.html',
scope: {
label: '#',
placeholder: '#',
name: '#',
form: '=',
ngModel: '=ngModel'
},
}
});
this is my template
<div class="form-group" ng-class="{'has-error': form.{{name}}.$invalid && form.{{name}}.$dirty, 'has-success': form.{{name}}.$valid }">
<label for="{{name}}" class="col-sm-2 control-label">{{label}}</label>
<div class="col-sm-10">
<input type="{{type}}" class="form-control col-sm-10" id="{{name}}" name="{{name}}" ng-model="ngModel" placeholder="{{placeholder}}" ng-maxlength="10" ng-required="true">
</div>
</div>
and this is in my index.html
<my-input ng-model="website.url" name="url" label="URL" placeholder="http://example.com" form="form"></my-input>
Even though the input inside the template is required, the field is validated, which it shouldn't be if it's empty.
What am I doing wrong?
Required fields are immediately invalid on DOM load, as they are not valid until they have a value.
Also, in your plunker the top input is green, with an illusion of valid while the lower input is red showing invalid as it should.
You can't do this...
<div class="form-group" ng-class="{'has-error': form.{{name}}.$invalid && form.{{name}}.$dirty, 'has-success': form.{{name}}.$valid }">
The issue is with the form name field. When you do this...
<input type="{{type}}" class="form-control col-sm-10" id="{{name}}" name="{{name}}" ng-model="ngModel" placeholder="{{placeholder}}" ng-maxlength="10" ng-required="true">
name="{{name}} will actually compile to name: {{name}} in the form object. exmaple:
{{name}}: Constructor$dirty: false
$error: Object
$formatters: Array[1]
$invalid: true
$isEmpty: function (value) {
$modelValue: undefined
$name: "{{name}}"
$parsers: Array[1]...
This was taken from your plunker. Play around with console.log($scope.form) and look at the object.
under $error you will find that there are two inputs THIS form references, and neither of them are the expected input named 'url', they are in fact the 'title' input and your directive input '{{name}}' as seen here...
form: Constructor
$addControl: function (control) {
$dirty: false
$error: Object...
$name: "title"...
$name: "{{name}}"
Here is a forked plunker where I have already set up to console.log the form
Plunker
This shows that the form has no idea about any 'url' input. To get around this, you can write a custom directive for the input name field. Or just use the one I wrote below.
app.directive('myName', function(){
var myNameError = "myName directive error: "
return {
restrict:'A', // Declares an Attributes Directive.
require: 'ngModel', // ngModelController.
link: function( scope, elem, attrs, ngModel ){
if( !ngModel ){ return } // if no ngModel exists for this element
checkInputFormat(attrs); // check myName input for proper formatting.
var inputName = attrs.myName.match('^\\w+').pop(); // match upto '/'
assignInputNameToInputModel(inputName, ngModel);
var formName = attrs.myName.match('\\w+$').pop(); // match after '/'
findForm(formName, ngModel, scope);
} // end link
} // end return
function checkInputFormat(attrs){
if( !/\w\/\w/.test(attrs.myName )){ // should be in format "wordcharacters/wordcharacters"
throw myNameError + "Formatting should be \"inputName/formName\" but is " + attrs.myName
}
}
function assignInputNameToInputModel(inputName, ngModel){
ngModel.$name = inputName // adds string to ngModel.$name variable.
}
function addInputNameToForm(formName, ngModel, scope){
scope[formName][ngModel.$name] = ngModel; return // add input name and input object.
}
function findForm(formName, ngModel, scope){
if( !scope ){ // ran out of scope before finding scope[formName]
throw myNameError + "<Form name=" + formName + "'> element could not be found."
}
if( formName in scope){ // found scope[formName]
addInputNameToForm(formName, ngModel, scope)
return
}
findForm(formName, ngModel, scope.$parent) // recursively search through $parent scopes
}
});
// DIRECTIVE NOTES:
// This directive is used by simply including the following HTML element:
// <input my-name="email/profileForm">.
// In the example above, the input name is "email" and the form name
// that this input would be attached to would be named "profileForm"
// Like this...
// <form name="profileForm">
// <input my-name="email/profileForm">
// </form>
// Notice this "/" dividing the input name from the form name. This directive uses the '/'
// to separate the input name from the form name.
// Although it has not been tested yet, this directive should work with multi nested forms
// as well, as the recursive search only looks for the form name that was passed in, inwhich
// to bind the ngModel to.
// In this example, other form names would be skipped.
// <form name="profileForm">
// <form name="miniFormOne">
// <form name="miniFormTwo">
// <input my-name="email/profileForm">
// </form>
// </form>
// </form>
// The above example may not be the best behavior, but was just added as an example.
With this directive, you could use your nested directive input like this
<input type="{{type}}" class="form-control col-sm-10" id="{{name}}" my-name="{{ variableName + '/' + yourformnamevariable }}" ng-model="ngModel" placeholder="{{placeholder}}" ng-maxlength="10" ng-required="true">
The myName directive will get this: "url/form" in your case, then it will add the $name variable which will be 'url' to the ngModel for THAT input, as well as search up scope for the form named "form" and attach the ndModel to THAT form. The directive uses the '/' to separate names. The search is recursive and will travel until it finds the form name or error out with a message.
Of course when you create dynamic input names, how are you to call out
formName.dynamicInputName.$valid? as you don't know what the dynamic input names ARE!!!
So using form.{{name}}.$invalid && form.{{name}}.$dirty doesn't work unless you do this...
form.$valid && form.$dirty
But that won't work for each individual input. You can figure out your own way to do that...
I use another directive inside the input
Then in my-errors I just listen to the ngModel like after requiring ngModel, you can now call ngModel.$invalid then set a variable to change the CSS. If I get some time, I'll make a jsFiddle and link it here at the bottom. However, you can just use the class input.ng-invalid { border-color: red; } and the inputs will be red until they are valid.
If this doesn't clear things up, ask some more questions... We'll get it.
Hope this helpy's

Angular : Adding Attribute directive on Element Directive

For my project, i'm currently developping custom form / inputs directive.
For example, I have the following directive :
angular.module('myApp').directive("textField", function() {
return {
restrict: 'E',
replace: true,
templateUrl : "/common/tpl/form/text-field.html",
scope : {
label : "#",
model : "="
}
};
});
with the associated template :
<div class="form-group">
<label for="{{fieldId}}" class="col-lg-2 control-label">{{label |translate}}</label>
<div class="col-lg-10">
<input type="text" class="form-control" id="{{fieldId}}" ng-model="model" placeholder="{{label|translate}}">
</div>
</div>
I have many more custom fields ( date, select, double select, and so on... )
The usage is simple :
<text-field label="app.myLabel" model="myObj.myAttribute"></text-field>
The idea is to cleanup the main template by avoiding to verbosely add labels on every fields. Very common need i believe.
Now the problem :
Now I need to add custom validation to my input models.
I did a naive approach which was to create a validation directive :
directive('myValidation', function (){
return {
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
ngModel.$parsers.unshift(function (value) {
// do some validation
return value;
});
}
};
});
and then to use it like this :
<text-field label="app.myLabel" model="myObj.myAttribute" myValidation="validationOptions"></text-field>
But of course this doesnt work, simple because the text-field directive which is replace=true "erases" the validation directive on it's element.
Can someone tell one what is the correct approach to do "custom input with presentation" directive, while allowing the validation part to be declared on the directive ( here text-field ) and used on the directive's input ?
Per example, is there a way to say "attributes on my element directive will be 'copied' to inside my directive ?"
aka :
<text-field label="app.myLabel" model="myObj.myAttribute" **myValidation="validationOptions"**></text-field>
would result in :
<div class="form-group">
<label for="{{fieldId}}" class="col-lg-2 control-label">{{label |translate}}</label>
<div class="col-lg-10">
<input type="text" class="form-control" id="{{fieldId}}" ng-model="model" **myValidation="validationOptions"** placeholder="{{label|translate}}">
</div>
</div>
Or am I simply missing something ?
I would like to avoid using transclusion to resolve this issue, because this would oblige the form template to look like this :
<field label="myLabel">
<input type="text" class="form-control" id="{{fieldId}}" ng-model="model" placeholder= {{label|translate}}">
</field>
which is just uselessly verbose in my opinion. But i'm starting to ask myself if there really is another option ?
Maybe the trick can be done in the pre (or post ?) directve link function, where I would copy attributes/ directive from the text-field tag to it's child (input) tag ?
Could someone just light the way for me there ?
Could you try this:
Write a validate directive. This will have a controller that exposes an addValidationFunction(fn) and a getValidationFunction() methods.
Have the myValidation directive require the validate controller and call ctrl.addValidationFunction(myValidationImplementation) where myValidationImplementation is a function implementing the validation logic for this specific directive.
Write another directive, validateInner. This will require optionally the validate controller from its parent. This directive will also require the ngModel controller. If it finds the validate controller, it calls ctrl.getValidationFunction() and registers the function with the ngModel i.e.:
require: ["^?validate", "ngModel"],
link: function(scope,el,attrs,ctrls) {
if( ctrls[0] != null ) {
var validationFn = ctrls[0].getValidationFunction();
// register validationFn with ngModel = ctrls[1]
}
...
}
In the template of your textField:
<input validate-inner type="text" class="form-control" id="{{fieldId}}" ng-model="model" placeholder="{{label|translate}}">
Usage:
<text-field label="app.myLabel" model="myObj.myAttribute"
validate my-validation="validationOptions"></text-field>
NOTE 1: I am not sure if a replace:true directive wipes the other directives. If so, it is not consistent behaviour.
NOTE 2: The myValidation directive gets called as <xxx my-validation> (note camelCase → dash-case). If your code above is not a typo, then this is why <text-field> seems to wipe myValidation.

Categories