AngularJS directive inside a ng-repeat loop with variable-based templateUrl - angularjs

Ok, I'm kinda new to the whole AngularJS thing and I'm probably getting my hands dirtier than I should, but here's what I'm trying to do:
loop through an array of fields (array of objects actually) for a customer entity
call a directive that chooses the template from a field property
data-bind the actual customer field to the ng-model inside the template
display the field
Here's what I have so far:
looping HTML
<div ng-repeat="info in customerCtrl.personalInfoFields">
<edit-field info="info" model="customerCtrl.customer"></edit-field>
</div>
stripped down controller:
customerCtrl.personalInfoFields = [{'field':'NAME', 'type':'text'},
{'field':'SURNAME', 'type':'text'},
{'field':'MAIL', 'type':'email'}]
customerCtrl.customer = customersFactory.customerDetails; // filled through a Factory, works fine if I just draw every single field manually in the HTML
directive:
myApp.directive('editField', ['$http', '$compile', 'capitalizeFilter', function($http, $compile, $capitalizeFilter) {
return {
scope: {
info: "=",
model: "="
},
replace: true,
template: '<div ng-include="url"></div>',
link: function(scope, element, attrs){
scope.url = '/views/fields/edit'+$capitalizeFilter(scope.info.type)+'.html';
}
/*templateUrl: function(elem,attrs)
{
if(typeof attrs.info.type === "undefined")
return '/views/fields/editText.html';
return '/views/fields/edit'+attrs.info.type+'.html'
},*/
};
}]);
editText.html (the other files don't differ much right now, will do later)
<div class="col-xs-12 col-sm-6 col-md-4 col-lg-4">
<div class="form-group">
<label class="control-label">{{'customers.'+info.field | i18n | capitalize}}</label>
<input type="text" class="form-control" ng-model="model[info.field]"/>
</div>
</div>
Right now, inside the field I just get [object Object], if I pass the actual field itself as model instead of the whole customer object it displays fine but it isn't binded (I can change the field content but it won't reflect in the controller object)
The commented templateUrl part doesn't work, since AngularJS only compiles the URL once for the whole ng-repeat function so I'm stuck with the undefined variable -> editText for everyone
How can I successfully bind the fields while still grabbing the right template for each field type?

Problem solved, apparently calling
$compile(element.contents())(scope);
fixed the binding, but I still had to pass the whole customer object to the directive and then use
ng-model="model[info.field]"
to get the desired result

Related

Bind dynamically set ng-model attribute for text field

After searching around for hours I am still unable to find an answer to my problem. I am populating a dynamic form with text fields based on values from a database, but am unable to successfully bind the fields to my model. Here's the scenario:
I've got a "project" model in my controller containing lots of project related information (name, start date, participants, category etc), but let's just focus on the "project.name" property for now. In the database I configure "forms" with a number of associated fields, where each field has a property that points to which property it corresponds to in my view model (e.g. "project.name"). At runtime I add these fields to an HTML form dynamically and attempt to set the ng-model attribute to the "modelBinding" value, in this case "project.name".
<div ng-repeat="formField in form.formFields">
<input ng-model="formField.modelBinding" /></div>
This will result in a text box being added to my form, with ng-model="formField.modelBinding" and the textbox value = 'project.data'.
What I am trying to achieve is to set ng-model = 'project.data', in other words replace formField.modelBinding with the value of formField.modelBinding.
One approach that seemed logical was
<input ng-model = "{{formField.modelBinding}}" />
but this is obviously not going to work. I've tried to insert the HTML tags with ng-bind-html but this seems to only work with ng-bind, not ng-model.
Any suggestions?
Assuming that you are trying to bind a value to a model from a name that you have within the formField you can create a directive (aka ngModelName) to bind your model by name from this value.
Observation: My first thought was using a simple accessor like model[formField.modelBinding] which would simple bind the formField.modelBinding into a model member on scope. However, I didn't use the property accessor because it would create a property named by formField.modelBinding value and not the correct object hierarchy expected. For example, the case described on this question, project.data would create an object { 'project.data': 'my data' } but not { 'project': { data: 'my data'}} as it should.
angular.module('myApp', [])
.directive('ngModelName', ['$compile', function ($compile) {
return {
restrict: 'A',
priority: 1000,
link: function (scope, element, attrs) {
scope.$watch(attrs.ngModelName, function(ngModelName) {
// no need to bind a model
if (attrs.ngModel == ngModelName || !ngModelName) return;
element.attr('ng-model', ngModelName);
// remove ngModel if it's empty
if (ngModelName == '') {
element.removeAttr('ng-model');
}
// clean the previous event handlers,
// to rebinded on the next compile
element.unbind();
//recompile to apply ngModel, and rebind events
$compile(element)(scope);
});
}
};
}])
.controller('myController', function ($scope) {
$scope.form = {
formFields: [
{
modelBinding: 'model.project.data'
}
]
};
$scope.model = {};
});
angular.element(document).ready(function () {
angular.bootstrap(document, ['myApp']);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-controller="myController">
<div ng-repeat="formField in form.formFields">
<input ng-model-name="formField.modelBinding" placeholder="{{ formField.modelBinding }}" />
</div>
<div>
<pre>{{ model | json }}</pre>
</div>
</div>
I guess the "modelBinding" attribute has the model name of the formfield, so, in that case you should do this:
<div ng-repeat="formField in form.formFields">
<input ng-model="form.formFields[formField.modelBinding]" />
</div>
Use the modelBinding as the key to retrieve from formFields.

angular directive (2-way-data-binding) - parent is not updated via ng-click

I have a nested directive with an isolated scope. An Array of objects is bound to it via 2 way data binding.
.directive('mapMarkerInput',['mapmarkerService', '$filter', '$timeout',function(mapMarkerService, $filter, $timeout) {
return {
restrict: 'EA',
templateUrl:'templates/mapmarkerInputView.html',
replace: true,
scope: {
mapmarkers: '='
},
link: function($scope, element, attrs) {
//some other code
$scope.addMapmarker = function($event) {
var mapmarker = {};
var offsetLeft = $($event.currentTarget).offset().left,
offsetTop = $($event.currentTarget).offset().top;
mapmarker.y_coord = $event.pageY - offsetTop;
mapmarker.x_coord = $event.pageX - offsetLeft;
mapmarker.map = $scope.currentMap;
$scope.mapmarkers = $scope.mapmarkers.concat(mapmarker);
};
$scope.deleteMapmarker = function(mapmarker) {
var index = $scope.mapmarkers.indexOf(mapmarker);
if(index !== -1) {
$scope.mapmarkers.splice(index,1);
}
};
//some other code
)
}]);
These 2 functions are triggered via ng-click:
<img ng-if="currentMap" ng-click="addMapmarker($event)" ng-src="/xenobladex/attachment/{{currentMap.attachment.id}}" />
<div class="mapmarker-wrapper" ng-repeat="mapmarker in shownMapmarkers" ng-click="setZIndex($event)" style="position: absolute; top: {{mapmarker.y_coord}}px; left: {{mapmarker.x_coord}}px;">
<!-- some other code -->
<div class="form-group">
<label>Name:</label>
<input ng-model="mapmarker.name" value="mapmarker.name" class="form-control" type="text">
</div>
<div class="form-group">
<label>Description:</label>
<input ng-model="mapmarker.description" value="mapmarker.description" class="form-control" type="text">
</div>
<button class="btn btn-danger" ng-click="deleteMapmarker(mapmarker)">Delete</button>
</div>
As you can see I am binding the name and description directly via ng-model and that works just fine. The properties are also available in the parent scope, but neither the delete nor the add works (its changed within the directives scope, but not the parent scope).
As far as I understand these changes should be applied, because I'm calling these functions via ng-click and I have other examples where this works. The only difference is, that I am binding to an array of objects and not a single object / property.
I tried using $timer and updateParent() ($scope.$apply() does not work -> throws an exception that the function is already within the digest cycle) but with no success, so it looks like these changes are not watched at all.
The directive code looks like this:
<map-marker-input ng-if="$parent.formFieldBind" mapmarkers="$parent.formFieldBind"></map-marker-input>
It is nested within a custom form field directive which gets the correct form field template dynamically and has therefore template: '<div ng-include="getTemplate()"></div>' as template, which creates a new child scope - that's why the $parent is needed here.
The binding definitely works in one way, the expected data is available within the directive and if I'm logging the data after changing it via delete or add, it's also correct, but only from the inside of the directive.
Because ng-model works I guess there might be a simple solution to the problem.
UPDATE
I created a plunkr with a simplified version:
http://plnkr.co/85oNM3ECFgCzyrSPahIr
Just click anywhere inside the blue area and new points are added from within the mapmarker directive. Right now I dont really prevent adding points if you delete or edit these - so you'll end up with a lot of points fast ;-)
There is a button to show the data from the parent scope and from the child scope.
If you edit the name or description of the one existing point that will also be changed in the parent scope (bound via ng-model). But all new points or deletions are ignored (bound within the functions called via ng-click).
If you want to update the parent scope, you need to access it via $parent once more,
i change
mapmarkers="$parent.formFieldBind"
to :
mapmarkers="$parent.$parent.formFieldBind"
ng-include create one more scope, so you need to access the parent once more.
http://plnkr.co/edit/27qF6ABUxIum8A3Hrvmt?p=preview

How to use a dynamically generated value in a template in AngularJS

I have a custom form application written in AngularJS and now I need to use the data from the form in a template. But nothing I've tried seems to work.
I am creating a custom directive like this...
.directive('dynamicModel', ['$compile', function ($compile) {
return {
'link': function(scope, element, attrs) {
scope.$watch(attrs.dynamicModel, function(dynamicModel) {
if (attrs.ngModel == dynamicModel || !dynamicModel) return;
element.attr('ng-model', dynamicModel);
if (dynamicModel == '') {
element.removeAttr('ng-model');
}
// Unbind all previous event handlers, this is
// necessary to remove previously linked models.
element.unbind();
$compile(element)(scope);
});
}
};
}])
This is attached to a form element like this..
<div class="row" ng-repeat="field in customForm.fields">
<label>{{field.displayname}}
<input class="form-control" type="{{field.type}}" name={{field.variable}} dynamic-model="field.databind" placeholder="{{field.variable}}" required="{{field.isRequired}}"></label></div>
This part works great, the field is now 2 way bound to the input form.
However when I later tried to use the same method to show the value in a report computed from the form, I get "field.databind" or at best the resolved databind field name such as "currentUser.name" rather than the value, e.g. "devlux"
I've tried
<div class="row" ng-repeat="field in customForm.fields">
<p>{{field.name}} = {{field.databind}}</p>
Also
<p dynamicModel="field.databind"></p>
</div>
Nothing works unless I put it into an input element, which isn't what I'm trying to do here.
The dynamic model code was pulled off someone elses answer to a question about creating dynamic form elements, and honestly I think it's just a step beyond my comprehension. But assuming that "field.databind" will always be a string literal containing the name of an inscope model, how on earth do I access it in a normal template?
{{field.databind}} will be evaluated against the current $scope and will result in whatever $scope.field.databind is, for example the string currentUser.name.
Angular has no way of knowing that currentUser.name isn't the string you want, but actually another expression that you want to evaluate.
To evaulate it again you will need to add a function to your $scope that uses the $parse service.
For example:
$scope.parseValue = function (value) {
return $parse(value)($scope);
};
In HTML:
<div class="row" ng-repeat="field in customForm.fields">
<p>{{field.displayname}} = {{parseValue(field.databind)}}</p>
</div>
The argument that gets passed to parseDynamicValue will for example be currentUser.name. Then it uses the $parse service to evaulate the expression against the current $scope, which will result in for example devlux.
Demo: http://plnkr.co/edit/iPsGvfqU0FSgQWGwi21W?p=preview

Directive: I bound isolate scope ("=") to template ngModel, but input won't update on controller

What I'm trying to do is put the isolate scope 'pointers' directly onto the ngModel within the template. What I expected was for the scope variables to update automatically on the parent controller. What ended up happening was both variables are constantly being evaluated as undefined.
I'm uncertain why this isn't working as expected, because having a two-way bound isolate scope means that for each created name, that name is automatically gets put on the scope (e.g. I have inputA, so scope.inputA is create).
I created a log function that runs every time something in the input changes, and I output the input the results... I keep getting undefined for the isolate scope variables inputA and inputB. I'm perplexed; what have I done incorrectly?
template
<div ng-app="app">
<div ng-controller="MyCtrl">
<div my-directive input-A="input.inputA" input-B="input.inputB"></div>
</div>
<br/>
<!-- output input -->
{{ input | json }}
</div>
directive/controller
angular.module("app")
.controller("MyCtrl", function($scope){ })
.directive("myDirective", function(){
return {
scope: {
inputA: "=",
inputB: "="
},
template: 'inputA: <input ng-model="inputA" ng-change="log(inputA)"/><br/>'
+'inputB: <input ng-model="inputB" ng-change="log(inputB)"/>',
link: function (scope, element, attrs) {
scope.log = function (theInput) {
console.log(theInput); // outputs correctly
console.log(scope.inputA); // outputs 'undefined'
console.log(scope.inputB); // outputs 'undefined'
};
}
}
});
I think the directive should be called as:
<div my-directive input-a="input.inputA" input-b="input.inputB"></div>
Note the lower-case input-a and input-b.

Ng-controller on same element as ng-repeat - no two-way-data-binding

I can't get two-way-data-binding to work in an Angular js ng-repeat.
I have an ng-controller specified on the same element that has the ng-repeat specified -
I just learnt that by doing this, I can get a hold of each item that is being iterated over by ng-repeat. Here is the HTML:
<div ng-controller="OtherController">
<div id="hoverMe" ng-controller="ItemController" ng-mouseenter="addCaption()"
ng-mouseleave="saveCaption()" ng-repeat="image in images">
<div class="imgMarker" style="{{image.css}}">
<div ng-show="image.captionExists" class="carousel-caption">
<p class="lead" contenteditable="true">{{image.caption}}</p>
</div>
</div>
</div>
</div>
And here is the ItemController:
function ItemController($scope){
$scope.addCaption = function(){
if($scope.image.captionExists === false){
$scope.image.captionExists = true;
}
}
$scope.saveCaption = function(){
console.log($scope.image.caption);
}
}
And the OtherController:
function OtherController($scope){
$scope.images = ..
}
When I hover the mouse over the #hoverMe-div - the caption-div is added correctly. But when I input some text in the paragraph and then move the mouse away from the #hoveMe-div, the $scope.image-variables caption value is not updated in the saveCaption-method. I understand I'm missing something. But what is it?
You don't need a ng-controller specified on the same element that has the ng-repeat to be able to get each item.
You can get the item like this:
<div ng-repeat="image in images" ng-mouseenter="addCaption(image)" ng-mouseleave="saveCaption(image)" class="image">
And in your controller code:
$scope.addCaption = function (image) {
if(!image.captionExists){
image.captionExists = true;
}
};
To get contenteditable to work you need to use ng-model and a directive that updates the model correctly.
Here is a simple example based on the documentation:
app.directive('contenteditable', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, controller) {
element.on('blur', function() {
scope.$apply(function() {
controller.$setViewValue(element.html());
});
});
controller.$render = function(value) {
element.html(value);
};
}
};
});
Note that the directive probably needs more logic to be able to handle for example line breaks.
Here is a working Plunker: http://plnkr.co/edit/0L3NKS?p=preview
I assume you are editing the content in p contenteditable and are expecting that the model image.caption is update. To make it work you need to setup 2 way binding.
2 way binding is available for element that support ng-model or else data needs to be synced manually. Check the ngModelController documentation and the sample available there. It should serve your purpose.

Resources