ng-model is not being exposed outside of directive - angularjs

I am using angular accordion directive in a form. Unfortunately, the model inside the input field value could not be exposed or display outside of the directive. Not really sure which part is wrong.
Fiddler link:Click here
Html
<body ng-app="btst">
<h3>BootStrap Accordion Directives</h3>
<form>
<pre>{{ form | json }}</pre> <!-- Not Working -->
<btst-accordion>
<btst-pane title="<b>First</b> Pane">
<pre>{{ form | json }}</pre> <!-- Working -->
Text: <input type="text" ng-model="form.textInput1"><br>
Date: <input type="date" ng-model="form.dateInput1">
</btst-pane>
<btst-pane title="<b>Second</b> Pane">
<pre>{{ form | json }}</pre> <!-- Working -->
Text: <input type="text" ng-model="form.textInput2"><br>
Number: <input type="date" ng-model="form.numberInput2">
</btst-pane>
</btst-accordion>
</form>
</body>
Directive
angular.module("btst", []).
directive("btstAccordion", function () {
return {
restrict: "E",
transclude: true,
replace: true,
scope: {},
template:
"<div class='accordion' ng-transclude></div>",
link: function (scope, element, attrs) {
// give this element a unique id
var id = element.attr("id");
if (!id) {
id = "btst-acc" + scope.$id;
element.attr("id", id);
}
// set data-parent on accordion-toggle elements
var arr = element.find(".accordion-toggle");
for (var i = 0; i < arr.length; i++) {
$(arr[i]).attr("data-parent", "#" + id);
$(arr[i]).attr("href", "#" + id + "collapse" + i);
}
arr = element.find(".accordion-body");
$(arr[0]).addClass("in"); // expand first pane
for (var i = 0; i < arr.length; i++) {
$(arr[i]).attr("id", id + "collapse" + i);
}
},
controller: function () {}
};
}).
directive('btstPane', function () {
return {
require: "^btstAccordion",
restrict: "E",
transclude: true,
replace: true,
scope: {
title: "#",
category: "=",
order: "="
},
template:
"<div class='accordion-group' >" +
" <div class='accordion-heading'>" +
" <a class='accordion-toggle' data-toggle='collapse'> {{category.name}} - </a>" +
" </div>" +
"<div class='accordion-body collapse'>" +
" <div class='accordion-inner' ng-transclude></div>" +
" </div>" +
"</div>",
link: function (scope, element, attrs) {
scope.$watch("title", function () {
// NOTE: this requires jQuery (jQLite won't do html)
var hdr = element.find(".accordion-toggle");
hdr.html(scope.title);
});
}
};
})

There are several issues. First, you have to define the form object on the scope. Since you want to see it outside of the btstAccordion directive (see the <pre> element), you have to define it in a separate controller:
function myCtrl($scope) {
$scope.form = {};
}
and then use that controller in your HTML:
<form ng-controller="myCtrl">
This will make <pre>{{ form | json }}</pre> behave like you expect it to.
Next, you're isolating the scope on your btstAccordion directive, which means that the scope outside of the directive isn't visible inside the directive:
directive("btstAccordion", function () {
return {
restrict: "E",
transclude: true,
replace: true,
scope: {}, // <= isolated scope
You can either
remove the isolate scope
pass the scope.form object as a parameter to the directive
Demo of first approach http://jsfiddle.net/rLksvezd/4/

Related

AngularJS: How to implement a directive that outputs its markup?

DEMO
Imagine I have some markup, e.g.:
<my-input model="data.firstName"></my-input>
Now, I would like to create a my-markup directive that will add a button to show/hide its markup.
So, this:
<div my-markup>
<my-input model="data.firstName"></my-input>
</div>
should result in this:
and when the button is clicked, the markup should appear:
The my-markup directive should not break any data bindings of its children.
Here is my attempt to implement this.
The markup appears, but the button doesn't work. Any ideas how to fix this?
PLAYGROUND HERE
Here is my approach. Couple of things:-
1) Instead of isolated scope on myMarkup, create a child scope, ultimately the actual directive myInput will be isolated. This would be required if you do need to support multiple myMarkup directive under the same scope.
2) You need a click event on the button, i wouldn't do logic on the markup instead abstract out to a method on the scope.
3) You would just need one button, do not need 2 buttons. Just change the text of the button.
.directive('myMarkup', function($compile) {
return {
restrict: 'A',
scope: true, //Create a child scope
compile: function(element) {
//Just need one button
var showButton = '<button ng-click="toggleMarkup()">{{model.showMarkup ? "Hide": "Show"}} Markup</button>';
var markup = '<pre ng-show="model.showMarkup">' + escapeHtml(element.html()) + '</pre>';
//append the markups
element.append(showButton).append(markup);
return linker;
}
};
function linker(scope, element) {
scope.model = {
showMarkup: false
};
//Click event handler on the button to toggle markup
scope.toggleMarkup = function(){
scope.model.showMarkup = !scope.model.showMarkup;
}
};
});
Demo
Please see below
function escapeHtml(html) {
return html.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
angular.module('App', []).controller('AppCtrl', function($scope) {
$scope.data = {
firstName: 'David'
};
}).directive('myInput', function() {
return {
restrict: 'E',
scope: {
model: '='
},
template: '<input class="my-input" type="text" ng-model="model">'
};
}).directive('myMarkup', function() {
return {
restrict: 'A',
scope: {},
link: function(scope, elem, attr) {
},
compile: function(element) {
var showButton = '<button ng-if="data.showMarkup" ng-click="data.showMarkup=!data.showMarkup">Hide Markup</button>';
var hideButton = '<button ng-if="!data.showMarkup" ng-click="data.showMarkup=!data.showMarkup">Show Markup</button>';
var markup = '<pre ng-if="data.showMarkup">' + escapeHtml(element.html()) + '</pre>';
element.append(showButton);
element.append(hideButton);
element.append(markup);
return function(scope, element) {
scope.data = {
showMarkup: true
};
};
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="App" ng-controller="AppCtrl">
<pre>data = {{ data | json }}</pre>
<div my-markup>
<my-input model="data.firstName"></my-input>
</div>
</body>

How to create a angular input directive that works with form validation

How do I create an angular directive that adds a input in a form but also works with form validation.
For instance the following directive creates a text input:
var template = '\
<div class="control-group" ng-class="{ \'error\': {{formName}}[\'{{fieldName}}\'].$invalid}">\
<label for="{{formName}}-{{fieldName}} class="control-label">{{label}}</label>\
<div class="controls">\
<input id="{{formName}}-{{fieldName}}" name="{{fieldName}}" type="text" ng-model="model" />\
</div>\
</div>\
';
angular
.module('common')
.directive('formTextField', ['$compile', function ($compile) {
return {
replace: true,
restrict: 'E',
scope: {
model: '=iwModel',
formName: '#iwFormName',
fieldName: '#iwFieldName',
label: '#iwLabel'
},
transclude: false,
template: template
};
}])
;
However the input is not added to the form variable ($scope.formName). I've been able to work around that by using the dynamicName directive from the following stackoverflow question Angularjs: form validation and input directive but ng-class will still not work.
Update
It now seems to be working but it feels like a hack. I thought I needed the scope to grab the form and field names however I can read that directly from the attributes. Doing this shows the field in the controllers scope form variable. However the ng-class attribute does not apply. The only way around this is to add the html element a second time once the scope is available.
jsFiddle here
var template = '\
<div class="control-group">\
<label class="control-label"></label>\
<div class="controls">\
<input class="input-xlarge" type="text" />\
</div>\
</div>\
';
angular
.module('common')
.directive('formTextField', ['$compile', function ($compile) {
return {
replace: true,
restrict: 'E',
scope: false,
compile: function compile(tElement, tAttrs, transclude) {
var elem = $(template);
var formName = tAttrs.iwFormName;
var fieldName = tAttrs.iwFieldName;
var label = tAttrs.iwLabel;
var model = tAttrs.iwModel;
elem.attr('ng-class', '{ \'error\': ' + formName + '[\'' + fieldName + '\'].$invalid }');
elem.find('label').attr('for', formName + '-' + fieldName);
elem.find('label').html(label);
elem.find('input').attr('id', formName + '-' + fieldName);
elem.find('input').attr('name', fieldName);
elem.find('input').attr('ng-model', model);
// This one is required so that angular adds the input to the controllers form scope variable
tElement.replaceWith(elem);
return {
pre: function preLink(scope, iElement, iAttrs, controller) {
// This one is required for ng-class to apply correctly
elem.replaceWith($compile(elem)(scope));
}
};
}
};
}])
;
when I do something like this, I use the directive compile function to build my html prior to it being processed. For example:
myApp.directive('specialInput', ['$compile', function($compile){
return {
// create child scope for control
scope: true,
compile: function(elem, attrs) {
// use this area to build your desired dom object tree using angular.element (eg:)
var input = angular.element('<input/>');
// Once you have built and setup your html, replace the provided directive element
elem.replaceWith(input);
// Also note, that if you need the regular link function,
// you can return one from here like so (although optional)
return function(scope, elem, attrs) {
// do link function stuff here
};
}
};
}]);

How to use defined text/ng-template in AngularJS directive

I try to write a very flexible directive. For doing so i want the user to define a template used in part of my return (as seen in the ui-bootstrap typeahead directive).
So i define my template like this:
<script type="text/ng-template" id="myDirectivesCustomTemplate.html">
<ul>
<li ng-repeat="value in values">
<a ng-click="doSomething(value.id)">
{{value.name}}
</a>
</li>
</ul>
</script>
I set this template in my directive
<div
my-directive
my-directive-custom-template="myDirectivesCustomTemplate.html"
my-directive-data="someScopeData">
Now in my directive, how can i render the custom template and use it with the passed data? When i try to use it to return in template directly it throws a ReferenceError: $scope is not defined Error. If i call it without scope, it says ReferenceError: myDirectiveCustomTemplate is not defined Error.
Where and how can i use my template if i do not just want to use it as a return directly?
EDIT: let's say, this is my directive:
(function() {
'use strict';
var Combobox = function() {
var displayInputField = elem.find('input.dropdown');
scope.$watch(scope.nsdComboboxModel,function(newVal){
/* search for newVal in given data object */
scope.setDisplayInputValue(newVal);
});
scope.setDisplayInputValue = function(value)
{
displayInputField.val(value);
};
scope.elementSelected = function (item, model, label) {
scope.ComboboxCallback(item);
scope.setDisplayInputValue(label);
};
}
return {
restrict: 'A',
transclude: true,
scope: {
Combobox: '#', /* used as HTML/CSS-id/name/path */
ComboboxModel: '=', /* the actual AngularJS model (ng-model) */
ComboboxAutocompleteData: '=', /* the data used for autoComplete (must be array of objects having id and value) */
ComboboxDropdownData: '=', /* data used by the dropdown template */
ComboboxCallback: '=', /* a callback function called with selected autocomplete data item on select */
ComboboxLabel: '#', /* label for the input field */
ComboboxDropdownTemplate: '#' /* label for the input field */
},
template:
'<section class="-combobox-directive container-fluid">' +
'<label for="{{Combobox}}" ng-if="ComboboxTranslation" translate="{{ComboboxLabel}}"></label>' +
'<div class="combobox input-group">' +
'<input type="text" ' +
'id="{{Combobox}}" ' +
'autocomplete="off" ' +
'ng-model="ComboboxDestinationDisplay" ' +
'data-toggle="dropdown" ' +
'typeahead="value as location.value for location in ComboboxAutocompleteData | filter:$viewValue" ' +
'typeahead-editable="false" ' +
'typeahead-on-select="elementSelected($item, $model, $label)" ' +
'class="form-control dropdown">' + // dropdown-toggle
'<span data-toggle="dropdown" class="input-group-addon dropdown-toggle">' +
'<span class="glyphicon glyphicon-globe"></span>' +
'</span>' +
//$compile(ComboboxDropdownTemplate) +
'</div>' +
'</section>',
link: link
};
};
angular.module('vibe.directives').directive('nsdCombobox', [NsdCombobox]);
})();
HTML
<script type="text/ng-template" id="myDirectivesCustomTemplate.html">
<ul>
<li ng-repeat="value in values">
<a ng-click="doSomething({id:value.id})">
{{value.name}}
</a>
</li>
</ul>
</script>
<div ng-controller="MainCtrl">
<div my-directive template="myDirectivesCustomTemplate.html" mydata="mydata" mycallback="doSomething(id)"></div>
</div>
JS
app.controller('MainCtrl',function($scope){
$scope.mydata = [{id:1,name:'One'},{id:2,name:'Two'},{id:3,name:'Three'}];
$scope.doSomething = function(id){
alert(id);
}
});
app.directive('myDirective', function($templateCache,$compile) {
return {
restrict: 'A',
scope:{
template : "#",
mydata : "=",
mycallback:"&"
},
link: function(scope,element) {
var template = $templateCache.get(scope.template);
scope.values = scope.mydata;
scope.doSomething = scope.mycallback;
element.append($compile(template)(scope));
}
}
});
Looking at your directive i can suggest try ng-include. Where you want to do
//$compile(ComboboxDropdownTemplate) +
'</div>'
change it to
<span ng-include='templateUrlPropertyOnScope'>
'</div>'
templateUrlPropertyOnScope property should point to a template either on server side or in script section created with type=text/ng-template.
I know this is 4 years later but if anyone still has this question, perhaps this answer may also be found useful.
With a simple directive such as this:
<my-directive template="custom-template.html"></my-directive>
You can then refer to the template attribute in your directive, like this:
(function() {
angular
.module('app')
.directive('myDirective', myDirective);
function myDirective() {
return {
restrict: 'E',
templateUrl: function(elem, attrs) {
return attrs.template;
}
}
}
}
you can use $http and $compile to achieve such a task.
app.directive('myDirective', function($http, $templateCache, $compile) {
return {
scope: {
// reference to your data.
// Just use `data.values` or `data.whatever` in your template
data: '=myDirectiveData'
},
link: function(scope, elm, attrs) {
// Load the template via my-directive-custom-template attribute
$http.get(attrs.myDirectiveCustomTemplate, {cache: $templateCache}).success(function(html) {
// update the HTML
elm.html(html);
// compile the html against the scope
return $compile(elm.contents())(scope);
});
}
};
});
I hope it gives you a good start

AngularJS - Different template in directive

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>',
};
});

angular, in directive, adding to the template an element with ng model

I'm trying to add an input element with ng-model inside a directive.
my code
the link function of my directive:
link: function (scope, element, attrs) {
var elem_0 = angular.element(element.children()[0]);
for (var i in scope.animals[0]) {
elem_0.append(angular.element('<span>' + scope.animals[0][i].id + '</span>'));
//this part doesn't work
var a_input = angular.element('<input type="text">');
a_input.attr('ng-model', 'animals[0][' + i + '].name');
//end
elem_0.append(a_input);
}
it seems i need to call $compile() at the end, but have no idea how.
Try
var a_input = angular.element($compile('<input type="text" ng-model="animals[0][' + i + '].name"/>')($scope))
elem_0.append(a_input);
You are making directive more complicated than necessary by manually looping over arrays when you could use nested ng-repeat in the directive template and let angular do the array loops:
angular.module("myApp", [])
.directive("myDirective", function () {
return {
restrict: 'EA',
replace: true,
scope: {
animals: '=animals'
},
template: '<div ng-repeat="group in animals">'+
'<span ng-repeat="animal in group">{{animal.id}}'+
'<input type="text" ng-model="animal.name"/>'+
'</span><hr>'+
'</div>'
}
});
DEMO: http://jsfiddle.net/Ajsy7/2/

Resources