I have a directive (element) that I created for a "switch", that uses input[checkbox].
As long as the initial state of the checkbox is to be NOT checked, everything works fine. But if I want to set the initial state to checked (based on my controller value), then it's always the opposite. Why is the value not checked when the controller variable says it should be?
Html
<a-switch toggle-state="vm.doSomething"></a-switch>
Directive Html
<div class="switch-container">
<input type="checkbox" id="switch" />
<label for="switch" class="pm-switch"></label>
</div>
Javascript controller
vm.doSomething = {
state: true
};
Directive
angular.module('material')
.directive('aSwitch', [
'$timeout', function($timeout) {
return {
templateUrl: 'elements/material/switch/switch.html',
transclude: false,
restrict: 'E',
scope: {
toggleState: '=',
},
link: function (scope, element) {
element.on('click touchstart', function (event) {
if (event.srcElement && event.srcElement.id && event.srcElement.id === "switch") {
event.stopPropagation();
$timeout(function() {
scope.toggleState.state = !scope.toggleState.state;
});
}
});
}
};
}
]);
I realize that in order to set the checked state of a generic
<input type="checkbox" />
I just need to add the attribute "checked", like
<input type="checkbox" checked />
but how do I do that if it's inside my directive's html?
As you are using isolated scope which has scope: { toggleState: '='} in it, you should have directly bind that toggleState.state to the input box inside template, so that the link function code will get remove as toggleState has been two way binded with your controller scope variable using toggle-state attribute
Directive.html
<div class="switch-container">
<input type="checkbox" id="switch" ng-model="toggleState.state"/>
<label for="switch" class="pm-switch"></label>
</div>
Related
Brief intro to my problem
I have a directive that dynamically shows a list of checkboxes. It has a parameter called options that should be an array like the following, in order to show the list of checkboxes correctly. For example:
var options = [
{
id: 1,
label: 'option #1'
},
{
id: 2,
label: 'option #2'
},
{
id: 3,
label: 'option #3'
}
];
So, by passing this array to my directive, a group of three checkboxes would be shown.
Also, the directive requires ngModel that will have the result of checking/unchecking the checkboxes (this object is always passed initialized). For example:
var result = {
"1": true,
"2": true,
"3": false
};
This case means that the first and second checkboxes (options with id=1 and id=2) are checked and the third (option with id=3) is unchecked.
My directive
template.html
<div ng-repeat="option in options track by $index">
<div class="checkbox">
<label>
<input type="checkbox"
ng-model="result[option.id]">
{{ ::option.label }}
</label>
</div>
</div>
directive.js
angular
.module('myApp')
.directive('myDirective', myDirective);
function myDirective() {
var directive = {
templateUrl: 'template.html',
restrict: 'E',
require: 'ngModel',
scope: {
options: '='
},
link: linkFunc
};
return directive;
function linkFunc(scope, element, attrs, ngModel) {
scope.result;
ngModel.$render = setResult;
function setResult() {
scope.result = ngModel.$viewValue;
};
};
};
What I want to achieve
Wherever I use my directive, I want to be able to trigger a function whenever the ngModel changes. Of course, I would like to achieve this using ngChange. So far I have the following:
<my-directive
name="myName"
options="ctrlVM.options"
ng-model="ctrlVM.result"
ng-change="ctrlVM.selectionChanged()">
</my-directive>
but the .selectionChanged() function is not triggered whenever the model changes. Anyone has any idea why this is not working as I am expecting it to work?
First thing first, please try to provide jsfiddle, codepen etc code snippet link so that it will be easy for others to answer your question.
The problem in your case is that you are never updating the ctrlVM.result object as you are passing the object's reference and that reference never change even if you manually update the model by calling ngModel.$setViewValue().
To solve the problem, just update the model by manually calling ngModel.$setViewValue() and pass in the new Object so that the reference changes and that will trigger the ngChange directives logic.
I've added the logic to do that and it will successfully trigger the change. Look at the code below:
angular
.module('myApp', [])
.directive('myDirective', myDirective)
.controller('MyController', function($timeout) {
var vm = this;
vm.options = [{
id: 1,
label: 'option #1'
}, {
id: 2,
label: 'option #2'
}, {
id: 3,
label: 'option #3'
}];
vm.result = {
"1": true,
"2": true,
"3": false
};
vm.selectionChanged = function() {
vm.isChanged = true;
$timeout(function() {
vm.isChanged = false;
}, 500)
}
});
function myDirective() {
var directive = {
templateUrl: 'template.html',
restrict: 'E',
require: 'ngModel',
scope: {
options: '='
},
link: linkFunc
};
return directive;
function linkFunc(scope, element, attrs, ngModel) {
scope.result;
ngModel.$render = setResult;
function setResult() {
scope.result = ngModel.$viewValue;
};
scope.updateValue = function(val) {
ngModel.$setViewValue(Object.assign({}, val))
}
};
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular.min.js"></script>
<div ng-app="myApp">
<script type="text/ng-template" id="template.html">
<div ng-repeat="option in options track by $index">
<div class="checkbox">
<label>
<input type="checkbox"
ng-model="result[option.id]" ng-click="updateValue(result)">
{{ ::option.label }}
</label>
</div>
</div>
</script>
<div ng-controller="MyController as ctrlVM">
<my-directive name="myName" options="ctrlVM.options" ng-model="ctrlVM.result" ng-change="ctrlVM.selectionChanged()">
</my-directive>
<div> Data: {{ctrlVM.result}} </div>
<div> isChanged: {{ctrlVM.isChanged}} </div>
</div>
</div>
#Gaurav correctly identified the problem (ng-change is never called because the object reference does not change). Here is a simpler solution that doesn't require manually cloning into the controller's model:
Add a binding for the ng-change attribute:
scope: {
options: '=',
ngChange: '&' // Add this, creates binding to `ctrlVM.selectionChanged()`
}
Add an ng-change to your checkbox template:
<input type="checkbox"
ng-model="result[option.id]" ng-change="ngChange()">
Now, when any checkbox changes it will automatically call the outer ng-change function without the intermediate step of cloning into the model.
I've written a directive which adds a class based on a condition - see snippet at the bottom of the question.
It works as expected in the following simple usage scenario for a required field:
<input type="text" name="lastName" ng-model="$crtl.lastName" my-directive="$crtl.isLastNameValid()" required>
However in the following scenario where I have two dependent elements using ng-required it blocks input on the element in which I don't type initially.
i.e. if I type in email it blocks input into mobile and visa versa - other than that is works fine, used as:
<input type="email" id="email" name="email" ng-model="$ctrl.emailAddress"
ng-required="$ctrl.mobileNumber.length === 0" my-directive="$ctrl.isEmailValid()">
<input type="tel" id="mobile" name="mobile" ng-model="$ctrl.mobileNumber"
pattern="(?:\+?61|0)4 ?(?:(?:[01] ?[0-9]|2 ?[0-57-9]|3 ?[1-9]|4 ?[7-9]|5 ?[018]) ?[0-9]|3 ?0 ?[0-5])(?: ?[0-9]){5}"
ng-required="$ctrl.emailAddress.length === 0" my-directive="$ctrl.isMobileValid()">
Where am I going wrong? I am compiling the element based on the condition passed in I am assuming it has something to do with that?
export const myDirective = ($compile: ng.ICompileService): ng.IDirective => {
return {
restrict: 'A',
scope: true,
compile: (element: ng.IAugmentedJQuery, attrs: ng.IAttributes): ng.IDirectivePrePost => {
var condition = attrs['myDirective'];
element.removeAttr('my-directive');
if (condition) {
element.attr('ng-class', `{ "validation-error": ${condition} }`);
return {
pre: () => { },
post: ($scope: ng.IScope, element: ng.IAugmentedJQuery) => {
$compile(element)($scope);
}
};
}
return {
pre: () => { },
post: () => { }
};
}
};
};
If you want a directive that adds and removes a class based on a condition defined by an angular expression:
app.directive("myDirective", function () {
return function postLink (scope, elem, attrs) {
scope.$watch(attrs.myDirective, function(newBool) {
if (newBool) {
attrs.$addClass("validation-error");
} else {
attrs.$removeClass("validation-error");
};
});
};
});
On every digest cycle, the directive evaluates the Angular Expression defined by the my-directive attribute and if the expression changes, it either adds or removes the validation-error class based in the truthyness of the Angular Expression.
How can I fill an input text field with a dropdown-menu ?
Text Input:
<input type="text" ng-model="storagePlaceModel" lms-dropdown class="form-control lms-input-text-disable lms-dropdown-toggle" id="storagePlace" placeholder="Standort" autocomplete="off" readonly>
Own written dropdown:
<div class="lms-dropdown">
<div class="lms-dropdown-scrollbar">
<li ng-repeat="data in data_input_fields.input_cat_music_book_storage_place">
<span>{{data.input_value}}</span>
<i ng-show="storagePlaceModel == data.input_value" class="glyphicon glyphicon-ok"></i>
</li>
</div>
</div>
If I select an li element I want to update the storagePlaceModel with the li value.
Updated question:
Because I have more than one of these text inputs with dropdowns I need a solution where the conroller/directive does not know the model name exactly.
Directive could look like:
lmsApp.directive('updateInputField', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
$(elem).click(function(e) {
// Read out the model name of the html text input field
});
}
};
});
Thank you for your help! I would appreciate every answer.
I've edited the entire question to create a directive to wrap your desired structure. You'll pass to the directive the model you want, and that way, each model will be independent on different directive usages:
myApp.directive('myDirective', function() {
return {
restrict: "E",
scope: {
model: "=",
datas: "="
},
templateUrl: "directive.html",
link: function(scope, element, attr) {
scope.updateValue = function(val) {
scope.model.storagePlaceModel = val;
}
}
}
});
The directive.html contains your text input and the dropdown.
Controller:
function MyCtrl($scope) {
$scope.wrapper = {};
$scope.wrapper2 = {};
$scope.wrapper3 = {};
$scope.datas = [
{ "input_value" : "1" },
{ "input_value" : "2" },
{ "input_value" : "3" },
{ "input_value" : "4" }
];
}
HTML usage:
<div ng-app="myApp" ng-controller="MyCtrl">
<my-directive model="wrapper" datas="datas"></my-directive>
<my-directive model="wrapper2" datas="datas"></my-directive>
<my-directive model="wrapper3" datas="datas"></my-directive>
</div>
Working Fiddle
I want to change the source (e.g source variable) that is managed by input's ngModel from my directive.
I want to clone the object that is editable by a form and make this process in the directive.
Probably this jsfiddle will explain you better.
What I do is:
<div ng-app="myApp">
<div ng-controller="SimpleController">
<form change-original-model>
<input ng-model="myModel.firstname" /><br/>
<input ng-model="myModel.secondname" /><br/>
I want <b>myModel</b> to be not changed:<br/>
{{ myModel.firstname }}<br/>
I want <b>newModel</b> to be cloned and changed by input:<br/>
{{ newModel.firstname }}<br/>
</form>
</div>
angular.module('directives', []);
angular.module('directives').controller('SimpleController', function($scope) {
$scope.myModel = { firstname: 'Sady', secondname: 'Sherozi' };
});
angular.module('directives').directive('changeOriginalModel',
function($parse) {
return {
require: '?ngModel',
link: function(scope, element, attrs, controller) {
var ngModel = $parse(attrs.ngModel)(scope);
scope.newModel = angular.copy(ngModel);
$(element).find("input").each(function(){
form_elements = $(this).attr("ng-model");
if (form_elements) {
var replace_input_data = form_elements.replace(attrs.ngModel + '.', "newModel." );
$(this).attr("ng-model", replace_input_data);
}
});
}
};
}
);
angular.module('myApp', ['directives']);
I am trying create a wrapper directive over select and I am trying to assign the 'name 'attribute to the select
directive
<form name=myform>
<selectformfield label="Select Orders" id="id_1" name="orderselection"
selectedval="obj.order" options="Orders" />
</form>
I have my directive defined as
mainApp
.directive(
'selectformfield',
function() {
return {
restrict : 'E',
transclude : true,
scope : {
label : '#',
id : '#',
selectedval : '=',
options : '=',
name: '='
},
template : "<select class='form-control' ng-model='selectedval' name='{{name}}' ng-options='item as item.name for item in options' required><option value=''>-- select --</option></select>"
};
});
I am trying to access the select's name attribute through myform in the controller something like console.log($scope.myForm.orderselection) and I get undefined
If I hardcode the name in the directive then I am able to access the attribute console.log($scope.myForm.orderselection)
I am missing anything here. Do I have to do any post compile or something ?
Khanh TO is correct in that you need to setup your name correctly when trying to access to through your isolated scope. Here is a working example of what I believe you are trying to accomplish. I've added comments to the code where I've changed what you had.
plunker
Javascript:
var app = angular.module('plunker', [])
.controller('MainCtrl', function ($scope, $log) {
$scope.model = {
person: {
name: 'World'
},
people: [{
name: 'Bob'
}, {
name: 'Harry'
}, {
name: 'World'
}]
};
})
.directive('selectformfield', function ($compile) {
return {
restrict: 'E',
replace: true, // Probably want replace instead of transclude
scope: {
label: '#',
id: '#',
selectedval: '=',
options: '=',
name: '#' // Change name to read the literal value of the attr
},
// change name='{{ name }}' to be ng-attr-name='{{ name }}' to support interpolation
template: "<select class='form-control' ng-model='selectedval' ng-attr-name='{{name}}' ng-options='item as item.name for item in options' required><option value=''>-- select --</option></select>"
};
});
HTML:
<body ng-controller="MainCtrl">
<p>Hello {{ model.person.name}}!</p>
<form name='myForm'>
<label for='orderselection'>Say hello to: </label>
<selectformfield label="Select Orders" id="id_1" name="orderselection"
selectedval="model.person" options="model.people"></selectformfield>
<p ng-class='{valid: myForm.$valid, invalid: myForm.$invalid }'>The form is valid: {{ myForm.$valid }}</p>
<p ng-class='{valid: myForm.orderselection.$valid, invalid: myForm.orderselection.$invalid }'>The people select field is valid: {{ myForm.orderselection.$valid }}</p>
</form>
</body>
CSS:
.valid {
color: green;
}
.invalid {
color: red;
}
Accessing the DOM directly in $scope is bad practice and should be avoided at all costs. In MVC structure like angular, instead of accessing the DOM (view) to get its state and data, access the models instead ($scope). In your case, you're binding the name of your directive to the orderselection property of your parent scope. Also notice that a form is an instance of FormController. The form instance can optionally be published into the scope using the name attribute. In your case, you create a new property on the parent scope.
You could try accessing the name like this if you're in your parent scope:
console.log( $scope.myform.orderselection );
Or if you're in your directive scope.
console.log( $scope.name);
Because your scope directive name property binds to your parent scope orderselection property, you need to assign a value to your parent scope property or it will be undefined. Like this:
$scope.myform.orderselection = "orderselection ";
If you need to do validation inside your directive, since you already bind the name attribute with the orderselection. You could do it like this:
template : "<select class='form-control' ng-attr-name='{{name}}' ng-disabled='[name].$invalid' .../>