2-way binding inside directive loop - angularjs

I am trying to write a simple form builder for my clients. The idea being for them to create a simple form that they can use later on different occasions.
For that I am at this point creating a directive to parse the json form back to html trough a directive.
angular
.module('myApp')
.directive('formbuilder', ['$timeout', '$compile', '$parse', function($timeout, $compile, $parse) {
return {
restrict:'AE',
require: 'ngModel',
scope: {
form: '=ngModel'
},
link: function(scope, element, attrs) {
$timeout(function() {
var bones = scope.form.structure;
scope.formId = scope.form.title.replace(/\W+/g, " ").replace(/\s/g,'').toLowerCase();
var html = '<form id="{{formId}}" class="row">';
angular.forEach(bones, function(bone, key) {
if(bone.type == 'text' || bone.type == 'checkbox') {
scope[key] = $parse('form.data.'+key)(scope);
html += '<input-field class="col s12"><input type="'+bone.type+'" id="'+bone.type+key+'" ng-model="form.data['+key+']" /> <label for="'+bone.type+key+'">'+bone.label+'</label></input-field> ';
}
})
html += '<p>{{form.data.input1}}</p>';
html += '</form>';
element.append($compile(html)(scope));
})
}
};
}]);
Problem is: I am looping through the items to find parse them back into html. That is clearly not working as expected. I can $parse it but then the 2-way binding is lost...
Any thoughts ?
The json structure is:
$scope.form = {
title: 'My first form',
structure: {
input1: {
type: 'text',
label: 'Input label'
},
input2: {
type: 'checkbox',
label: 'This is a checkbox'
},
input3: {
type: 'checkbox',
label: 'This is a CHECKED checkbox'
}
},
data: {
input1: 'Yannick',
input2: false,
input3: true
}
}

I would avoid using the ng-model attribute which instantiates the ngModelController. Instead use one-time binding:
<formbuilder form="::form"></formbuilder>
In the directive, use isolate scope with one-way (<) binding:
.directive('formbuilder', ['$timeout', '$compile', '$parse', function($timeout, $compile, $parse) {
return {
restrict:'AE',
/*
require: 'ngModel',
scope: {
form: '=ngModel'
},
*/
scope: { form: "<" },
This will bind the form object reference to the isolate scope. Changes to the contents by the directive inputs will be shared with the parent scope object. There is no need to do two-way binding of the object reference.
Also for obvious reasons, don't use bracket notation for the property accessor, instead use dot notation:
//html += '<input ng-model="form.data['+key+']"'
//USE dot notation
html += '<input ng-model="form.data.'+key+'"'
The DEMO on PLNKR.

Related

AngularJS directive isolate scope and parent scope

I'm trying to implement a recursive directive and it seems like to get it to work nicely I need to define an isolate scope as well as have access to the parent scope. Basically I want my directive to have access to variables set as attributes on the directive itself, but i also want to be able to access variables and methods set in the controller's scope. Is there a way to combine the two? I've tried with transclude but i suppose i'm not entirely sure if i've used it properly. Here is a fiddle example of my problem, where i'd like each 'child' in the directive to be able to call the function sayHi(): http://jsfiddle.net/n8dPm/655/
You have to pass the sayHi function to your directive.
Directives create their own scope, So sayHi function is not part of your directive scope, the way to allow it is by creating a new prop an pass it.
HTML
<div ng-app="myapp">
<div ng-controller="TreeCtrl">
<tree family="treeFamily"
say-hi="sayHi(name)"
ngTransclude></tree>
</div>
</div>
JS
var module = angular.module('myapp', []);
module.controller("TreeCtrl", function($scope) {
$scope.treeFamily = {
name : "Parent",
children: [{
name : "Child1",
children: [{
name : "Grandchild1",
children: []
},{
name : "Grandchild2",
children: []
}]
}, {
name: "Child2",
children: []
}]
};
$scope.sayHi = function(name){
alert(name+' says hello!')
}
});
module.directive("tree", function($compile) {
return {
restrict: "E",
scope: {
family: '=',
sayHi : '&'
},
transclude: true,
template:
'<p>{{ family.name }}</p>'+
'<ul>' +
'<li ng-repeat="child in family.children">' +
'<tree family="child" say-hi="sayHi(name)"></tree>' +
'<button ng-click="sayHi({name : child.name})">Say Hi</button>' +
'</li>' +
'</ul>',
compile: function(tElement, tAttr) {
var contents = tElement.contents().remove();
var compiledContents;
return function(scope, iElement, iAttr) {
if(!compiledContents) {
compiledContents = $compile(contents);
}
compiledContents(scope, function(clone, scope) {
iElement.append(clone);
});
};
}
};
});

Selected item in directive not working

I created a select directive and am using this directive twice. I need to see the selected items of both. What should I do?
HTML
<div select-list="items"></div>
<div select-list="items2"></div>
Controller
var myApp = angular.module('myApp',[]);
myApp.controller('mainController', function($scope) {
$scope.items = [
{
name: "1"
},
{
name: "2"
}
];
$scope.items2 = [
{
name: "3"
},
{
name:"4"
}
];
$scope.selectedValues = [];
});
Select directive
myApp.directive("selectList", function() {
return {
restrict: "EACM",
template: '<select ng-model="selectedValues" ng-options="item.name for item in data"></select>',
scope: {
data: '=selectList'
}
}
});
I need to add selected items of both "selects" into $scope.selectedValues.
I tried through ng-change, but it didn't work.
Your directive use isolated scope, so you can't access from the controller to the directive or from the directive to the controller.
You have to create a new entry.
I let you a fiddle that is working :
https://jsfiddle.net/Lv1q2sh2/1/
// Code goes here
var myApp = angular.module('app', []);
angular.module('app')
.directive("selectList", function(){
return {
restrict: "EACM",
require: 'ngModel',
template: '<select ng-model="selected" ng-change="onSelectedValue()" ng-options="item.name for item in data"></select>',
scope: {
data: '=selectList'
},
link: function (scope, element, attr, ngModel) {
scope.onSelectedValue = function () {
ngModel.$setViewValue(scope.selected);
}
}
}
})
.controller('mainController', function($scope) {
$scope.items = [
{name: "1"},
{name: "2"}
];
$scope.items2 = [
{name:"3"},
{name:"4"}
];
$scope.selectedValues = [];
});
Directive needs to be created properly:
Have a controller for your directive
If you are using isolated scope, make sure to pass selectedValue to the scope.
ex:
Directive:
myApp.directive("selectList", function(){
return{
restrict: "EACM",
template: '<select ng-model="selectedValues" ng-options="item.name for item in data"></select>',
scope: {
data: '=selectList',
ngModel: '='
}
//Add link function here, crate watcher on ngModel and update it back on select dropdown selection.
})};
HTML:
<div select-list="items" ng-model="selectedValue1" ></div>
<div select-list="items2" ng-model="selectedValue2"></div>
Add link function to directive and put a watcher on ngModel, once user makes change in selection, update parent ng-model.

Data binding not working in an event in directive

I cannot get the data binding to work in this directive. The variables are not bound properly when I change them inside the event handler,
What am I doing wrong? > Test Fiddle
var myApp = angular.module('myApp', [])
.directive('inputTest', function () {
return {
restrict: 'E',
template: '<div class="input-group">\
<input type="text" class="form-control" />\
<br>child scope: {{myValue}}\
</div>',
scope: {
myValue: '=',
},
link: function (scope, element, attrs) {
$(element).on('click', function (e) {
alert('c');
scope.myValue = 'clicked';
});
scope.myValue = 'not clicked';
},
};
})
function MyCtrl($scope) {
$scope.myValue = 'parent value';
}
HTML
<div ng-controller="MyCtrl">parent scope: {{myValue}}
<input-test my-value="myValue"></input-test>
</div>
Do not forget to call $scope.$apply() at the end of the event handler.
First level bindings may not work as expected due to how prototypical inheritance works. If you try the first point and still get no results, try putting myValue a level deeper:
$scope.data.myValue = 'parent value';
and:
<input-test my-value="data.myValue"></input-test>

How can I bind deep child property of directive's scope?

My dear friends,
We can bind a scope property of a directive to the value of DOM attribute.
This works:
module.directive 'MyDirective', ->
scope:
directiveVar: '='
...
<div class='MyDirective' directive-var='parentVar'></div>
In example above we bind directive's directiveVar property to parentVar property of the parent scope.
This is a bi-directional binding, so if directiveVar is changed parentVar is automatically updated, and vice versa.
My question is:
Is there a way I can bind a deep child property of my directive's scope instead? Like scope.lv1.directiveVar or scope.lv1.lv2.lv3.directiveVar instead of scope.directiveVar?
Docs I read
What I want to achieve
I have an object in directive scope named lv1. I want to bind its property directiveVar to parent property.
This does not work:
scope:
lv1:
directiveVar: '='
And this does not work:
scope:
"lv1.directiveVar": '=myVar'
Demo
This is what works: http://plnkr.co/edit/OClnZ2Cl3BXr60PC2qVP?p=preview
This is what I want to achieve: http://plnkr.co/edit/tQEHeKOzGjGyplCwUtU2?p=preview
I hope this code will help. You can pass in an object and watch its properties or you can nest things in parent/child directives. Either way adding the "=" will enable two way binding on the entire object.
Controller:
$scope.directiveVar = {
level1: {
greeting: 'Hello'
}
};
$scope.otherVar = {
levelA: {
foo: 'bar'
}
};
Markup:
<div data-my-parent-directive data-other-var="otherVar">
<div data-my-directive data-directive-var="directiveVar"></div>
</div>
Directive:
angular.module('MyDirective', [])
.directive('myParentDirective', ['$window',
function ($window) {
return{
restrict: 'AE',
scope:{
otherVar: '='
},
controller: function($scope, $element) {
var othis = this;
this.element = $element;
this.otherVar = $scope.otherVar;
}
};
}
])
.directive('myDirective', ['$window',
function ($window) {
return {
restrict: 'AE',
require: '?myParentDirective',
scope: {
directiveVar: '='
},
link: function(scope, elm, attr, myParentDirectiveCtrl) {
console.log(myParentDirectiveCtrl.otherVar);
console.log(myDirectiveParentCtrl.otherVar.levelA);
scope.$watch('directiveVar.level1', function (newValue, oldValue){
console.log(newValue, oldValue);
});
}
};
}
])
Edit:
You could simply do this:
<div data-my-parent-directive data-other-var="otherVar">
<div data-my-directive data-directive-var="directiveVar.level1"></div>
</div>
Which is useful when modeling data. For example:
var items = apiService.getItems();
var layers = [...],
$scope.map = {
points: items,
layers: layers,
highAltMap: 'Streets',
lowAltMap: 'Satellite'
};
That said if you are modelling data see this ng-conf video which I believe the speaker touched on some OO patterns.
Edit 2:
You could use a service like this plunker
I know date of the question, but...
Create json object in attribute
return {
scope: {
date: "=",
formChange: "=?",
ngDisabled: "=?",
deepInclude: "=?"
},
...
<user-date-without-current-date date="cv.dateBirthday" deep-include="{cnt:cv.dateBirthday}" form-change="$parent.form_change"></user-date-without-current-date>
Is it usefull?

Class directive not working when using ng-class to load the directive

I am trying to load a 'class' directive using ng-class. but my directive is never loaded when i do that. The directive is a multipurpose directive, and I don't want to create an isolated scope on this. it will be loaded only when required, based on ng-class conditions hence not using attribute or element directive. has anyone tried doing this and succeeded?
this directive is called using <div ng-class="someClass {{tooltip: enabled}}"></div>
here enabled is a scope variable.
app.directive('tooltip', ['$timeout', '$location', '$rootScope', function (timer, $location, $rootScope) {
return {
restrict: 'C',
transclude: true,
link: function (scope, element) {
var printContent = function () {
/* uses the content of .tooltip-content if it is a complex html tooltip,
otherwise
you can use the title attribute for plaintext tooltips
*/
var tooltipContent = $(element).find('.tooltip-content').html();
if (!tooltipContent) {
tooltipContent = $(element).attr('title');
}
$(element).tooltip({
content: tooltipContent,
items: "img, a, span, button, div",
tooltipClass: "tooltip",
position: { my: "left+30 top", at: "right top", collision: "flipfit" },
show: { effect: "fadeIn", duration: "fast" },
hide: { effect: "fadeOut", duration: "fast" },
open: function (event, ui) { $rootScope.tooltipElement = event.target; }
});
};
timer(printContent, 0);
}
};
}]);
Interesting issue. It seems that you don't want to use the ng-class directive since that doesn't recompile the content after adding the class. You'll likely want to create your own dynamic-class directive that actually recompiles when the value is true:
app.directive('dynamicClass', function($compile) {
return {
scope: {
dynamicClassWhen: '=',
dynamicClass: '='
},
link: function(scope, elt, attrs) {
scope.$watch('dynamicClassWhen', function(val) {
if (val) {
console.log(val);
elt.addClass(scope.dynamicClass);
$compile(elt)(scope);
}
});
}
};
});
You may need to modify this for the ability to remove the class and depending on if the $compile is sufficient for you or if you need to further manipulate the html, but this seems to be the right track for you. I made a fiddle with this in action.
Hope this helps!

Resources