How to get ng-model value inside custom directive - angularjs

I've searched here on SO and tried the answers I found, but I can't seem to get the model value out of the ngModel of my custom directive.
Here's the directive
/*
*usage: <markdown ng:model="someModel.content"></markdown>
*/
breathingRoom.directive('markdown', function () {
var nextId = 0;
return {
require: 'ngModel',
replace: true,
restrict: 'E',
template: '<div class="pagedown-bootstrap-editor"></div>',
link:function (scope, element, attrs, ngModel) {
var editorUniqueId = nextId++;
element.html($('<div>' +
'<div class="wmd-panel">' +
'<div id="wmd-button-bar-' + editorUniqueId + '"></div>' +
'<textarea class="wmd-input" id="wmd-input-' + editorUniqueId + '">{{modelValue()}}' +
'</textarea>' +
'</div>' +
'<div id="wmd-preview-' + editorUniqueId + '" class="wmd-panel wmd-preview"></div>' +
'</div>'));
var converter = new Markdown.Converter();
var help = function () {
// 2DO: add nice modal dialog
alert("Do you need help?");
};
var editor = new Markdown.Editor(converter, "-" + editorUniqueId, {
handler: help
});
editor.run();
// local -> parent scope change (model)
jQuery("#wmd-input-" + editorUniqueId).on('change', function () {
var rawContent = $(this).val();
ngModel.$setViewValue(rawContent);
scope.$apply();
});
// parent scope -> local change
scope.modelValue = function () {
console.log('modelvalue - ', ngModel.$viewValue);
return ngModel.$viewValue;
};
}
};
});
And here's the HTML
<markdown ng-class="{error: (moduleForm.Description.$dirty && moduleForm.Description.$invalid) || (moduleForm.Description.$invalid && submitted)}"
id="Description"
name="Description"
placeholder="Description"
ng-model="module.description"
required></markdown>
The problem here is that the output is simply
{{modelValue()}}
I also tried creating a private method
function getModelValue() {
console.log(ngModel.$viewValue);
return ngModel.$viewValue;
}
and then change the one template line to
'<textarea class="wmd-input" id="wmd-input-' + editorUniqueId + '">' + getModelValue() +
but then the output is
NaN
Where am I going wrong?
if it matters, here's the order of my scripts (not including vendor scripts)
<script src="app.js"></script>
<script src="directives/backButtonDirective.js"></script>
<script src="directives/bootstrapSwitchDirective.js"></script>
<script src="directives/markdownDirective.js"></script>
<script src="directives/trackActiveDirective.js"></script>
<script src="services/alertService.js"></script>
<script src="services/roleService.js"></script>
<script src="services/moduleService.js"></script>
<script src="services/changePasswordService.js"></script>
<script src="services/userService.js"></script>
<script src="controllers/usersController.js"></script>
<script src="controllers/userController.js"></script>
<script src="controllers/moduleController.js"></script>
<script src="controllers/modulesController.js"></script>

The HTML your inserting isn't getting compiled. It's easiest just to move it into your template, or investigate using ng-transclude. Here's an example of moving it into your template.
plunker
breathingRoom.directive('markdown', function () {
var nextId = 0;
return {
require: 'ngModel',
replace: true,
restrict: 'E',
template: '<div class="pagedown-bootstrap-editor"><div class="wmd-panel">' +
'<div id="wmd-button-bar-{{editorUniqueId}}"></div>' +
'<textarea class="wmd-input" id="wmd-input-{{editorUniqueId}}">{{modelValue()}}' +
'</textarea>' +
'</div>' +
'<div id="wmd-preview-{{editorUniqueId}}" class="wmd-panel wmd-preview"></div>' +
'</div></div>',
link:function (scope, element, attrs, ngModel) {
scope.editorUniqueId = nextId++;
// parent scope -> local change
scope.modelValue = function () {
console.log('modelvalue - ' + ngModel.$viewValue);
return ngModel.$viewValue;
};
}
};
});

You weren't actually resolving your model as the {{modelValue()}} expression was just part of the HTML string your were building in the link function.
You should move the editor markup to the template so that you can bind to ng-model.
Assuming the goal is to create the necessary HTML markup for the Markdown Editor and then show the preview of the converted markdown I would split this into two roles:
A directive for the custom markup
A filter for actually converting the value to markdown:
Markup:
<div ng-app="app" ng-controller="DemoCtrl">
<h3>Markdown editor</h3>
<markdown-editor ng-model="markdown"></markdown-editor>
</div>
JavaScript:
var app = angular.module('app', []);
app.filter('markdown', function () {
return function (input) {
return input.toUpperCase(); // this is where you'd convert your markdown
};
});
app.directive('markdownEditor', function () {
return {
restrict: 'E',
scope: {
ngModel: "="
},
template:
'<div>' +
'<textarea ng-model="ngModel"></textarea>' +
'<div class="preview">{{ ngModel | markdown }}</div>' +
'</div>'
};
});
app.controller('DemoCtrl', function ($scope) {
$scope.markdown = "**hello world**";
});
The = scope sets up a two way binding on the property passed to ng-model and {{ ngModel | markdown }} pipes the value of ngModel to the markdown filter.
http://jsfiddle.net/benfosterdev/jY3ZK/

Look at angular's parse service. It enables you get and set the value of property referenced in ng-model.
link: function(scope, elem, attrs) {
var getter = $parse(attrs.ngModel);
var setter = getter.assign;
var value = getter(scope);
setter(scope, 'newValue');
}

The simplest way is:
If ngModel variable is on the top level of the scope
link: function(scope, elem, attrs) {
if (attrs.ngModel) {
var myModelReference = scope[attrs.ngModel];
}
}
If ngModel refers to a property deeply nested on the scope (best way)
(so, for scope.prop1.prop2 , attrs.ngModel will be "prop1.prop2"), and since you can't just look up scope['prop1.prop2'], you need to dig in by converting the string key to actual nested keys.
For this I recommend Lodash _.get() function
link: function(scope, elem, attrs) {
if (attrs.ngModel) {
var myModelReference = _.get(scope, attrs.ngModel);
}
}

Related

How do I dynamically change class in Angular direcitves?

I'm trying to write a custom directive to replace similar buttons on my page. But when I move ng-class into directive's template, it's not working anymore. Is it wrong to include ng-class within custom directive? Should I use addClass and removeClass in link function instead?
html:
<dt-button ngclass="{'active-button': selectedRows.length >=1}" text="tablebuttons.delete" icon="v-delete" ng-click="deleteDialog()"></dt-button>
directive
.directive('dtButton', function() {
return {
restrict: 'E',
scope: {
icon: '#',
text: '#',
ngclass: '='
},
link: function(scope, ielem, iattrs) {
},
template:
'<button ng-class="{{ngclass}}">' +
'<span class="{{icon}}"></span>' +
'<p translate="{{text}}">' +
'</p>' +
'</button>'
}
})
try use this. change class to ng-class in your template.
you pass a model to directive for text in view while it is not 2 way data binding.
template:
'<button class="active-button" ng-class="{{ngclass}}">' +
'<span class="{{icon}}"></span>' +
'<p translate="{{text}}">' +
'</p>' +
'</button>'
// Code goes here
var app = angular
.module('MyApp', [])
.controller('Main', ['$scope',
function($scope) {
var vm = this;
vm.selectedRows = 4;
vm.deleteDialog = function() {
console.log(vm.selectedRows);
vm.selectedRows = 0;
}
}
])
.directive('dtButton', function() {
return {
restrict: 'E',
scope: {
icon: '#',
text: '#',
ngclass: '='
},
controller: "Main as ctrl",
link: function(scope, ielem, iattrs) {
},
template: '<button ng-class="ngclass" >' +
'<p>{{text}}</p>' +
'</button>'
}
});
.active-button {
background-color: green;
color: white;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div class="main-content" ng-app="MyApp" ng-controller="Main as ctrl">
<div>
<dt-button ngclass="{'active-button':ctrl.selectedRows >=1}" ng-click="ctrl.deleteDialog()" text="delete"></dt-button>
</div>
</div>
I think nothing wrong with your approach to put ng-class at template of directive. I have tried to reproduce your code snippet at this plunk it is give the correct class name active-button which i defined at style.css with background color blue. But because i don't know much about expression selectedRows.length >=1 on your ngclass attribute, i make it just to true value which will always give active-button class to the element. When you change it to false, it will remove the active-button class.
My guess is seem something wrong with your expression selectedRows.length >=1. At following element declaration :
<dt-button ngclass="{'active-button': selectedRows.length >=1}" text="tablebuttons.delete" icon="v-delete" ng-click="deleteDialog()"></dt-button>
Maybe you can check by bind those expression return value to the element with double curly brace or any other way.
Small correction for your code, you may need to put semicolon ( ; ) at the end of return keyword inside .directive().
Try This
jimApp = angular.module("mainApp", []);
jimApp.controller('mainCtrl', function($scope){
$scope.selectedRows = [0];
$scope.tablebuttons = {delete:"Delete"};
$scope.deleteDialog = function(){
$scope.selectedRows = [];
}
});
jimApp.directive('dtButton', function() {
return {
restrict: 'E',
scope: {
icon: '#',
text: '#',
myClass: '#'
},
link: function(scope, ielem, iattrs) {
console.log(scope.myClass);
},
template:
'<button class="{{myClass}}">' +
'<span class="{{icon}}"></span>' +
'{{text}}' +
'</button>'
}
})
.active-button{
background:red;
}
.inactive-button{
background:#ccc;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="mainApp" ng-controller="mainCtrl">
<dt-button my-class="{{selectedRows.length?'active-button':'inactive-button'}}" text="{{tablebuttons.delete}}" icon="v-delete" ng-click="deleteDialog()"></dt-button>
</div>

ngMessages dont work inside a directives template

My ngMessages doesnt work inside my directives template!
I have a directive myInput with a template and a link function, inside the template function I create template string for a wrapped <label> and <input>.
Inside the Link function I use the require: '^form' FormController and retrieve the form name. Then I'm putting a ngMessages block after the wrapped elements.
(function () {
'use strict';
angular
.module('app.components')
.directive('myInput', MyInput);
/*#ngInject*/
function MyInput($compile, ValidatorService, _, LIST_OF_VALIDATORS) {
return {
require: '^form',
restrict: 'E',
controller: MyInputController,
controllerAs: 'vm',
bindToController: true,
template: TemplateFunction,
scope: {
label: '#',
id: '#',
value: '=',
validateCustom: '&'
},
link: MyInputLink
};
function MyInputController($attrs) {
var vm = this;
vm.value = '';
vm.validateClass = '';
vm.successMessage = '';
vm.errorMessage = '';
}
function TemplateFunction(tElement, tAttrs) {
return '<div class="input-field">' +
' <label id="input_{{vm.id}}_label" for="input_{{vm.id}}" >{{vm.label}}</label>' +
' <input id="input_{{vm.id}}" name="{{vm.id}}" ng-class="vm.validateClass" type="text" ng-model="vm.value" >' +
'</div>';
}
function MyInputLink(scope, element, attrs, form){
var extra = ' <div ng-messages="' + form.$name + '.' + scope.vm.id + '.$error">' +
' <div ng-messages-include="/modules/components/validationMessages.html"></div>' +
' </div>';
$(element).after(extra);
}
}
})();
Usage:
<h1>Test</h1>
<form name="myForm">
<my-input label="My Input" id="input1" value="vm.input1"></my-input>
-------
<!-- this block is hardcoded and is working, it does not come from the directive! -->
<div ng-messages="myForm.input1.$error">
<div ng-messages-include="/modules/components/validationMessages.html"></div>
</div>
</form>
Instead of adding the ngMessages block inside the link function, add it inside the compile function.
It is not as handy as in the link funciton because of the missing FormController, but it works :-)
Here the code:
compile: function(tElement, tAttrs){
var name = tElement.closest('form').attr('name'),
fullFieldName = name + '.' + tAttrs.id; // or tAttrs.name
var extra = '<div ng-messages="' + fullFieldName + '.$error">' +
' <div ng-messages-include="/modules/components/validationMessages.html"></div>' +
'</div>';
$(element).after(extra);
Here is what I did, I added to scope, myForm: '=' then in the directive's template referred to <div ng-messages="vm.myForm[vm.id].$error" >
I feel this is much cleaner than mucking around in the link function.

Update ng-options hosted by a directive

I have a custom directive that displays a dropdownlist.
Is it possible to dynamically re-populate the ng-options from a datasource that comes from the controller that hosts the directive.
The datasource itself comes from a service.
Currently it works well from the initial array passed to the directive, but when I add new data (from the controller/service to this array I would like to update the item list.
Any help?
EDIT :
This is how I use my directive.
<select-item-obj-from-array datasource="ctrl.ActivityAddresses" ng-model="form.Activity.AddressID" name="AddressID" value="AddressID" label="City" .... />
My directive looks like:
app.directive('selectItemObjFromArray', function () {
return {
restrict: 'E',
replace: true,
template: function (element, attrs) {
var tpl = '';
tpl += "<div><div class=\"form-group clearfix\" >";
tpl += '<label for="' + attrs.name + '" class="col-lg-3 control-label">' + attrs.label + '</label>';
tpl += '<div class="col-lg-9">';
tpl += '<select ng-disabled="ngDisabled" name="' + attrs.name + '" ng-model="ngModel" chosen="datasource" ng-options="c.Name for c in datasource"></select>';
tpl += '</div>';
tpl += '</div>';
tpl += '</div>';
return tpl;
},
scope: {
ngModel: "=",
datasource: "="
},
link: function (scope, elem, attrs) {
var select = elem.find("select").eq(0);
select.chosen();
scope.$watch(function () {
return select[0].length;
},
function (newvalue, oldvalue) {
if (newvalue !== oldvalue) {
select.trigger("chosen:updated");
}
});
scope.$watch(attrs.ngModel, function () {
select.trigger('chosen:updated');
});
}
};
});
if my controller/service updated the ctrl.ActivityAddresses I don't know how to "reinvoke" the directive to update the dropdownlist..
You can broadcast from your service like this:
var broadcast = function() {
$rootScope.$broadcast('items.update');
};
Assuming that items is an array.
Then you can catch the broadcast in your controller or directive:
$scope.$on('items.update', function (event) {
//Do whatever you want with the items.
});
I think this is what you want? You don't need to change the ng-options directive for this.

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/

How to create a directive with a dynamic template in AngularJS?

How can I create a directive with a dynamic template?
'use strict';
app.directive('ngFormField', function($compile) {
return {
transclude: true,
scope: {
label: '#'
},
template: '<label for="user_email">{{label}}</label>',
// append
replace: true,
// attribute restriction
restrict: 'E',
// linking method
link: function($scope, element, attrs) {
switch (attrs['type']) {
case "text":
// append input field to "template"
case "select":
// append select dropdown to "template"
}
}
}
});
<ng-form-field label="First Name" type="text"></ng-form-field>
This is what I have right now, and it is displaying the label correctly. However, I'm not sure on how to append additional HTML to the template. Or combining 2 templates into 1.
i've used the $templateCache to accomplish something similar. i put several ng-templates in a single html file, which i reference using the directive's templateUrl. that ensures the html is available to the template cache. then i can simply select by id to get the ng-template i want.
template.html:
<script type="text/ng-template" id=“foo”>
foo
</script>
<script type="text/ng-template" id=“bar”>
bar
</script>
directive:
myapp.directive(‘foobardirective’, ['$compile', '$templateCache', function ($compile, $templateCache) {
var getTemplate = function(data) {
// use data to determine which template to use
var templateid = 'foo';
var template = $templateCache.get(templateid);
return template;
}
return {
templateUrl: 'views/partials/template.html',
scope: {data: '='},
restrict: 'E',
link: function(scope, element) {
var template = getTemplate(scope.data);
element.html(template);
$compile(element.contents())(scope);
}
};
}]);
Had a similar need. $compile does the job. (Not completely sure if this is "THE" way to do it, still working my way through angular)
http://jsbin.com/ebuhuv/7/edit - my exploration test.
One thing to note (per my example), one of my requirements was that the template would change based on a type attribute once you clicked save, and the templates were very different. So though, you get the data binding, if need a new template in there, you will have to recompile.
You should move your switch into the template by using the 'ng-switch' directive:
module.directive('testForm', function() {
return {
restrict: 'E',
controllerAs: 'form',
controller: function ($scope) {
console.log("Form controller initialization");
var self = this;
this.fields = {};
this.addField = function(field) {
console.log("New field: ", field);
self.fields[field.name] = field;
};
}
}
});
module.directive('formField', function () {
return {
require: "^testForm",
template:
'<div ng-switch="field.fieldType">' +
' <span>{{title}}:</span>' +
' <input' +
' ng-switch-when="text"' +
' name="{{field.name}}"' +
' type="text"' +
' ng-model="field.value"' +
' />' +
' <select' +
' ng-switch-when="select"' +
' name="{{field.name}}"' +
' ng-model="field.value"' +
' ng-options="option for option in options">' +
' <option value=""></option>' +
' </select>' +
'</div>',
restrict: 'E',
replace: true,
scope: {
fieldType: "#",
title: "#",
name: "#",
value: "#",
options: "=",
},
link: function($scope, $element, $attrs, form) {
$scope.field = $scope;
form.addField($scope);
}
};
});
It can be use like this:
<test-form>
<div>
User '{{!form.fields.email.value}}' will be a {{!form.fields.role.value}}
</div>
<form-field title="Email" name="email" field-type="text" value="me#example.com"></form-field>
<form-field title="Role" name="role" field-type="select" options="['Cook', 'Eater']"></form-field>
<form-field title="Sex" name="sex" field-type="select" options="['Awesome', 'So-so', 'awful']"></form-field>
</test-form>
One way is using a template function in your directive:
...
template: function(tElem, tAttrs){
return '<div ng-include="' + tAttrs.template + '" />';
}
...
If you want to use AngularJs Directive with dynamic template, you can use those answers,But here is more professional and legal syntax of it.You can use templateUrl not only with single value.You can use it as a function,which returns a value as url.That function has some arguments,which you can use.
http://www.w3docs.com/snippets/angularjs/dynamically-change-template-url-in-angularjs-directives.html
I managed to deal with this problem. Below is the link :
https://github.com/nakosung/ng-dynamic-template-example
with the specific file being:
https://github.com/nakosung/ng-dynamic-template-example/blob/master/src/main.coffee
dynamicTemplate directive hosts dynamic template which is passed within scope and hosted element acts like other native angular elements.
scope.template = '< div ng-controller="SomeUberCtrl">rocks< /div>'
I have been in the same situation, my complete solution has been posted here
Basically I load a template in the directive in this way
var tpl = '' +
<div ng-if="maxLength"
ng-include="\'length.tpl.html\'">
</div>' +
'<div ng-if="required"
ng-include="\'required.tpl.html\'">
</div>';
then according to the value of maxLength and required I can dynamically load one of the 2 templates, only one of them at a time is shown if necessary.
I heope it helps.

Resources