I've defined an Angular directive stField (<st-field>). It dynamically creates a DOM element, <st-field-vov>, and inserts it inside using $compile. I need this dynamic injection because there are other types of fields. The DOM would look like this:
<st-field>
<st-field-vov></st-field-vov>
</st-field>
stFieldVov is another custom directive I create, it handles the rendering of the concrete field. Here is the JS:
(function(module) {
'use strict';
module
.directive('stField', _stField)
.directive('stFieldVov', _stFieldVov);
/*ngInject*/
function _stField($compile, stFieldSvc) {
return {
restrict: 'E',
scope: {
stFieldTmpl: '=',
stDataObjects: '='
},
link: function(scope, $elem) {
var _fieldTmpl = scope.stFieldTmpl,
template = '<st-field-' + stFieldSvc.getFieldTypeShortName(_fieldTmpl.type) + '></div>';
$elem.append($compile(template)(scope));
}
};
}
function _stFieldVov() {
return {
restrict: 'E',
link: function(scope) {
var _fieldTmpl = scope.stFieldTmpl,
_dataObjects = scope.stDataObjects,
_isValue = true;
scope.dataObjectDropDownOptions = {
dataSource: new kendo.data.DataSource({
data: _dataObjects
}),
dataTextField: 'name',
dataValueField: 'id'
};
},
templateUrl: '/widgets/fields/directives/templates/vov.html'
};
}
})(angular.module('st.widgets.fields'));
Here is the template for stFieldVov:
<div class="input-group">
<input type="text" class="form-control" ng-show="isValue()">
<input kendo-drop-down-list
k-options="dataObjectDropDownOptions"
k-option-filter="contains">
<span class="input-group-btn">
<button type="button" class="btn"
ng-class="{'st-btn-do': isDataObject()}">
<span class="icon-server"></span>
</button>
</span>
</div>
The problem is that the k-options for configuring the Kendo widget, kendoDropDownList, doesn't work. I think this is because I used $compile to generate <st-field-vov> as k-options works well if use kendoDropDownList in <st-field>.
The browser doesn't throw any error, it is just that the drop down data is empty.
Thanks for reading.
Try to use this:
$elem.append(template);
$compile($elem.contents())(scope);
Related
My custom directive is "testapp". I would like to access the value of "file-model" in its template, which would contain the file object to be uploaded. Whatever I do am not able to get the value, its displaying "undefined". Please suggest ways to solve this problem.
app.directive('testapp', function ($compile) {
return {
restrict: 'E',
scope: {
text: '#',
filem: '=fileModel'
},
require:'fileModel',
replace:true,
template: '<div class="col-xs-4" style="padding-left:0px"><div class ="jumbotron" id="Section1"><input type="text" name="id" id="section{{incr}}" placeholder="Section Heading" class="form-control"><form id="addLIForm1"><div ng-repeat="i in range(fileIncr) track by $index"><input type="text" name="file{{incr}}{{$index+1}}" id="file{{incr}}{{$index+1}}" class="form-control" placeholder="File Name" style="width:50%" ><input type = "file" id="path{{incr}}{{$index+1}}" file-model = "file{{incr}}{{$index+1}}"></div></form><a id="addMore" ng-click="addfile()" href="#">Add More File</a><a id="addMoreSec1" ng-click="add()" class="pull-right" href="#" ng-show="showValue">Add More Section</a></div></div>',
controller: function ($scope, $element,$window) {
$scope.incr=$window.globalIncr;
$scope.fileIncr=$window.globalfileIncr;
$scope.showValue=$window.globalsectionValue;
$scope.add = function () {
$window.globalsectionValue=false;
$window.globalIncr+=1;
$window.globalfileIncr=1;
var el = $compile("<testapp></testapp>")($scope);
$element.parent().append(el);
//alert($window.globalfileIncr);
//alert($element.parent().parent());
};
$scope.range=function(n)
{
return new Array(n);
}
$scope.addfile=function(){
alert($scope.filem);
$scope.fileIncr+=1;
$window.counterObject[$scope.incr] = $scope.fileIncr;
};
}
};
});
I'm trying to wrap a ui-select in a custom directive. (https://github.com/angular-ui/ui-select)
this.adminv2.directive('eventSelect', function() {
return {
restrict: 'E',
replace: true,
scope: {
ngModel: '=',
placeholder: '='
},
controller: function($scope, $http) {
return $scope.refreshEvents = function(searchTerm) {
return $http.get('/events/autocomplete', {
params: {
term: searchTerm
}
}).then(function(response) {
return $scope.events = response.data;
});
};
},
template: "<div>{{ngModel}}\n <ui-select ng-model=\"ngModel\"\n theme=\"bootstrap\"\n ng-disabled=\"disabled\"\n reset-search-input=\"false\">\n <ui-select-match placeholder=\"Enter an event\">{{$select.selected.name}}</ui-select-match>\n <ui-select-choices repeat=\"event in events track by $index\"\n refresh=\"refreshEvents($select.search)\"\n refresh-delay=\"0\">\n <span ng-bind-html=\"event.name | highlight: $select.search\"></span>\n <i class=\"icon-uniF111 fg type-{{raceType}} pull-right\" ng-repeat='raceType in event.racetypes'></i>\n <br>\n {{event.dates}} <i class='pull-right'>{{event.location}}</i>\n </ui-select-choices>\n </ui-select>\n</div>"
};
});
The select works properly, but the binding with ng-model doesn't work. I cannot set the model or read it.
I don't get it since it works when I use a simple template such as
<div><input ng-model="ngModel"></div>
Is there something special to do because I wrap a directive in directive ?
I managed to make the binding work by setting the ng-model in the template as
ng-model="$parent.ngModel"
I have a form based on twitter bootstrap, each field have it's own configuration
// controller (the template shows this in ng-repeat
$scope.fields = [{name:"f1", label:"Field 1", with_button: false},
{name:"f2", label:"Field 2", with_button: true}]
I'm trying to make a "conditional directive" that customize the template according to "field.with_button"
// Without button
<div class="controls">
<input type="text" id="i_{{field.name}}">
</div>
// With button
<div class="controls">
<div class="input-append">
<input type="text" id="i_{{field.name}}">
<span class="add-on">bt</span>
</div>
</div>
I searched a lot and didn't find any solution, I tried to create only one div and put contents inside with a compiler function but it didn't parse, and if I call $apply it crashes.
How could I make this directive?
wrong My last try:
angular.module('mymodule',[]).directive('ssField', function() {
return {
transclude:false,
scope: {
field: '='
},
restrict: 'E',
replace:true,
template: '<div class="controls">{{innerContent}}</div>',
controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
$scope.$eval('$scope.innerContent = \'<input type="text" id="input_{{field.name}}" placeholder="{{field.name}}" class="input-xlarge">\'');
}]
};
});
//<ss-field field="{{field}}"></ss-field>
You can use the $http and $compile services to do what you're after.
http://plnkr.co/edit/Xt9khe?p=preview
This plnkr should demostrate what needs to be done, but basically:
Use $http to load the template depending on the condition.
Compile the loaded template against the current scope with $compile.
angular.module('mymodule',[]).directive('ssField', ['$http', '$compile', function($http, $compile) {
return {
transclude:false,
scope: {
field: '='
},
restrict: 'E',
replace:true,
template: '<div class="controls"></div>',
link: function(scope, element, attrs) {
var template;
var withButtonTmpl = 'with_button.html';
var withoutButtonTmpl = 'without_button.html';
if (scope.field.with_button) {
$http.get(withButtonTmpl).then(function(tmpl) {
template = $compile(tmpl.data)(scope);
element.append(template);
});
} else {
$http.get(withoutButtonTmpl).then(function(tmpl) {
template = $compile(tmpl.data)(scope);
element.append(template);
});
}
}
};
}]);
You can change the directive to be more robust so the URLs aren't directly embedded in the directive for re-usability, etc., but the concept should be similar.
Just to further expand on Cuing Vo's answer here is something similar to what I use(without using external partials and additional $http calls):
http://jsfiddle.net/LvUdQ/
myApp.directive('myDirective',['$compile', function($compile) {
return {
restrict: 'E',
template: '<hr/>',
link: function (scope, element, attrs, ngModelCtrl) {
var template = {
'templ1':'<div>Template 1</div>',
'templ2':'<div>Template 2</div>',
'default':'<div>Template Default</div>'
};
var templateObj;
if(attrs.templateName){
templateObj = $compile(template[attrs.templateName])(scope);
}else{
templateObj = $compile(template['default'])(scope);
}
element.append(templateObj);
}
};
}]);
However Im not quite sure its by the bible from performance perspective.
In AngularJS, directly manipulate the DOM must only be a last resort solution. Here, you can simply use the ngSwitch directive :
angular.module('mymodule',[]).directive('ssField', function() {
return {
transclude:false,
scope: {
field: '='
},
restrict: 'E',
replace:true,
template:
'<div class="controls" data-ng-switch="field.with_button">' +
'<input type="text" id="i_{{field.name}}" data-ng-switch-when="false">' +
'<div class="input-append" data-ng-switch-default>' +
'<input type="text" id="i_{{field.name}}">' +
'<span class="add-on">bt</span>' +
'</div>' +
'</div>',
};
});
I am using custom angular form validation to validate fields based off the field having a value AND the selected value of a drop down. I use the same section of code (and therefore the same directive) twice on the page. So I could do this re-use, I tried sending in an argument to the directive. This is all working fine. However when the child scope is created it breaks my 2-way binding back to fruit.cost.
Here is an example fiddle.
What am I doing wrong? I want all the validation to work the same but also preserve the 2-way binding. Here is a copy of my fiddle code:
JS
function MainCtrl($scope){
$scope.localFruits = [
{name: 'banana'},
{name: 'orange'},
{name: 'grape'}
];
$scope.fruits = [
{name: 'banana'},
{name: 'orange'},
{name: 'grape'}
];
$scope.costRequired = [
'local',
'overseas'
];
$scope.selectedCostRequired = '';
}
angular.module('test', []).directive('customRequired', function() {
return {
restrict: 'A',
require: 'ngModel',
scope: {
requiredWhenKey: '=',
cost: '=' // I think the problem is here?? Can't figure out how to pass the property I want to bind to
},
link: function(scope, elem, attrs, ctrl) {
//console.log(scope);
function isValid(value) {
if (scope.$parent.selectedCostRequired === scope.requiredWhenKey) {
return !!value;
}
return true;
}
ctrl.$parsers.unshift(function(value) {
var valid = isValid(value);
scope.$parent.subForm.cost.$setValidity('customRequired', valid);
return valid ? value : undefined;
});
ctrl.$formatters.unshift(function(value) {
scope.$parent.subForm.cost.$setValidity('customRequired', isValid(value));
return value;
});
scope.$watch('$parent.$parent.selectedCostRequired', function() {
scope.$parent.subForm.cost.$setValidity('customRequired', isValid(ctrl.$modelValue));
});
}
};
});
HTML
<div ng-app="test" ng-controller="MainCtrl">
<form name="form">
Which grouping is required? <select name="costRequired" ng-model="selectedCostRequired" ng-options="t for t in costRequired"></select>
<h2>Local</h2>
<div ng-repeat="fruit in localFruits" ng-form="subForm">
{{fruit.name}}: <input name="cost" ng-model="fruit.cost" required-when-key="'local'" custom-required/> bound value is: [{{fruit.cost}}]
<span class="error" ng-show="subForm.cost.$error.customRequired">Required</span>
</div>
<h2>Overseas</h2>
<div ng-repeat="fruit in fruits" ng-form="subForm">
{{fruit.name}}: <input name="cost" ng-model="fruit.cost" required-when-key="'overseas'" custom-required/> bound value is: [{{fruit.cost}}]
<span class="error" ng-show="subForm.cost.$error.customRequired">Required</span>
</div>
<div ng-show="form.$invalid" class="error">some form error(s) still exist</div>
<div ng-show="!form.$invalid" class="okay">form looks good!</div>
</form>
</div>
Using ng-model with a directive that creates an isolate scope on the same element doesn't work.
I suggest either not creating a new scope, or use scope: true.
Here is a simplified example that does not create a new scope:
<form name="form">
<div ng-repeat="fruit in localFruits">
{{fruit.name}}:
<input ng-model="fruit.cost" required-when-key="local" custom-required/>
bound value is: [{{fruit.cost}}]
</div>
</form>
function MyCtrl($scope) {
$scope.localFruits = [
{name: 'banana'},
];
}
app.directive('customRequired', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
console.log(attrs.requiredWhenKey);
}
};
});
fiddle
Since the required-when-key attribute is just a string, you can get the value using attrs.requiredWhenKey.
If you don't create a new scope, you should also be able to remove most of the $parent lookups in your directive.
I am implementing a form builder in AngularJS and need to insert and reorder directives at runtime.
Don't even know where to start looking - all examples seem to only demonstrate static tree of directives. Two options to achieve dynamic behaviour are: a) compiling and inserting templates on the fly and b) using huge ng-switch of all possible directives. Both ways are ugly.
Can anyone suggest a better implementation?
Below is JS and html code for how I think formbuilder should look in an ideal world, please help me fill in 3 instances of TODO.
JSFiddle JavaScript:
angular.module('components', [])
.directive('checkbox', function() {
return {
restrict: 'E',
template: '<div class=f><input type=checkbox>{{name}}</input></div>'
};
})
.directive('textfield', function() {
return {
restrict: 'E',
template: '<div class=f><input type=text placeholder="{{name}}"></input></div>'
};
})
function FormBuilder($scope, $locale) {
$scope.title = 'test form';
$scope.fields = [];
$scope.add_checkbox = function() {
console.log('adding checkbox');
var field = null; // TODO: how do I instantiate a directive?
$scope.fields.push(field);
};
$scope.add_textfield = function() {
console.log('adding textfield');
var field = null; // TODO: how do I instantiate a directive?
$scope.fields.push(field);
};
}
HTML:
<div ng-app=components ng-controller=FormBuilder>
<button ng:click="add_checkbox()">new checbox</button>
<button ng:click="add_textfield()">new text field</button>
<h3>{{ title }}</h3>
<checkbox></checkbox>
<textfield></textfield>
<div ng:repeat="field in fields">
<!-- TODO field.get_html() - how? -->
</div>
</div>
I think you have a couple ways to do this as you mentioned and since you don't want to do a switch you can create a template file for each directive. ie checkbox.html, textfield.html and put the directive in each one. Then populate your fields array with ['checkbox.html', 'textarea.html'] when you add in your loop you just simply <div ng-include='field'></div>
Here is a demo: http://plnkr.co/edit/w6n6xpng6rP5WJHDlJ3Y?p=preview
You could also create another directive where you pass in the input type and have it inject it into the template. Here is a demo of this which allows you to avoid having to declare templates and letting a directive create them based on the field type:
http://plnkr.co/jhWGuMXZTuSpz8otsVRY
<div ng:repeat="field in fields">
<master-field type='field'></master-field>
</div>
This master-field directive just compiles a template based on the value of field.
.directive('masterField', function($compile) {
return {
restrict: 'E',
replace:true,
transclude: true,
scope:{
type:'='
},
template: '<div></div>',
controller: function ( $scope, $element, $attrs ) {},
link: function(scope, element, attrs) {
element.append( $compile('<' + scope.type+ '/></' +scope.type + '>')(scope) );
}
};
})