Angular dynamic nested directive, passing a new scope to each child? - javascript

I'm trying to create a nested directive in angular, which is specified by a large, possibly deeply nested config object. I'm having trouble figuring out how to get the correct scope to the correct directive.
My example plunkr is here: http://plnkr.co/edit/AJVaSk2GSxIJvZx3B6UX?p=preview
I think that I could copy the current child config into an attribute on each child and get that via the scope attribute of the directive definition, but that seems a little crazy - it could be huge, and might need to be escaped.
Is there a better way to do this, or something else I've completely missed? (Or another SO question that answers this that I've missed...)
Very much obliged.
Edit from comment for clarification
I'm trying to get each directive to have a scope (or even just a variable on the scope?) for each sub-object. So 'theform' directive gets the top level object, each 'theforma' directive gets the child that created it. That is, I'd have one 'theforma' directive with the object containing 'type' : 'form_a_child_1', and the other would have the object containing 'type' : 'form_a_child_2'. So they'd be "self-contained" and only really know about the object they were created with.
Ultimately, then, I'd like a submit button, and a way to gather all of the information from the dynamic form elements...but that's for another question.

Got distracted by video games yesterday hope this wasn't terribly urgent to get resolved but I posted an answer here now. This is probably not exactly what you want to do but should get the idea across about isolate scope.
http://plnkr.co/edit/FmpixQrBmvWJlB2wBRXW?p=preview
JS
// Code goes here
var app = angular.module('formtest', []);
app.controller("MyCtrl", function($scope){
$scope.form_config = {
// first level children are the tabs
children : [
{
type : 'form_a',
label : 'Form A',
children : [
{
type : 'form_a_child_1',
label : 'Form A Child 1',
},
{
type : 'form_a_child_2',
label : 'Form A Child 2',
},
]
},
{
type : 'form_b',
label : 'Form B',
children : [
{
type : 'form_b_child_1',
label : 'Form B Child 1',
},
{
type : 'form_b_child_2',
label : 'Form B Child 2',
},
]
},
]
};
})
/**
* Should display the top level form info.
* Form A and Form B
*/
app.directive('theform', function() {
return {
restrict : 'E',
scope:{formConfig:"="},
template : '<div>'+
'<div>'+
'<div><theforma form-a="formAData"></div>'+
'<div><theformb form-b="formBData"></div>'+
'</div>'+
'</div>',
replace : true,
link : function(scope, $el, $attrs) {
//$scope.form_config = form_config;
scope.formAData = [];
scope.formBData = [];
scope.$watch("formConfig", function(newVal){
if(newVal)
{
for(var i=0; i<scope.formConfig.children.length; i++)
{
var curElement = scope.formConfig.children[i];
console.log(curElement);
if(curElement.type == "form_a")
scope.formAData = curElement;
else
scope.formBData = curElement;
}
}
console.log(scope.formAData);
})
}
};
});
/**
* I want to display the children of Form A
*/
app.directive('theforma', function() {
return {
restrict : 'E',
scope : {formA : "="},
template : '<div>'+
'<div ng-repeat="child in formA.children">'+
'Form A: {{child.label}}'+
'</div>' +
'</div>',
replace : true,
link : function(scope, $el, $attrs) {
//console.log(scope.formA)
}
};
});
/**
* I want to display the children of Form B
*/
app.directive('theformb', function() {
return {
restrict : 'E',
scope : {formB : "="},
template : '<div>'+
'<div ng-repeat="child in formB.children">'+
'Form B: {{child.label}}'+
'</div>' +
'</div>',
replace : true,
link : function(scope, $el, $attrs) {
scope.children = scope.formB;
}
};
});
HTML
<!DOCTYPE html>
<html>
<head>
<script data-require="angular.js#*" data-semver="1.2.14" src="http://code.angularjs.org/1.2.14/angular.js"></script>
<link rel="stylesheet" href="style.css" />
<script src="script.js"></script>
</head>
<body ng-app='formtest' ng-controller="MyCtrl">
<div>
<theform form-config="form_config"></theform>
</div>
</body>
</html>
When it comes to isolate scope for a directive you have three options for what you put in the quotes after a property name (=, &, or #). = makes the property two way bind, so if you pass in a Javascript object the changes are made to that object, # will get you a string result from the expression you pass in and pass through that string, & allows you to pass a reference to a function to be executed on the original scope (that of the controller or another directive that uses it).

Related

prevent reflecting ng-model value across all select tags

I am pretty new to AngularJS. I am working on a project wherein I need to append certain html select tags based on a button click. Each select tag is bound to a ng-model attribute (which is hardcoded). Now the problem I am facing is, once I append more than 2 such html templates and make changes in a select tag then value selected is reflected across all the tags bound to the corresponding ng-model attribute (which is pretty obvious). I would like to know if there is a way around it without naming each ng-model differently.
JS code:
EsConnector.directive("placeholderid", function($compile, $rootScope, queryService, chartOptions){
return {
restrict : 'A',
scope : true,
link : function($scope, element, attrs){
$scope.current_mount1 = "iscsi";
$scope.current_dedupe1 = "on";
$scope.y_axis_param1 = "Total iops";
var totalIops =[];
var totalBandwidth =[];
element.bind("click", function(){
$scope.count++;
$scope.placeholdervalue = "placeholder12"+$scope.count;
var compiledHTML = $compile('<span class="static" id='+$scope.placeholdervalue+'>choose mount type<select ng-bind="current_mount1" ng-options="o as o for o in mount_type"></select>choose dedupe<select ng-model="current_dedupe1" ng-options="o as o for o in dedupe"></select>choose y axis param<select ng-model="y_axis_param1" ng-options="o as o for o in y_axis_param_options"></select></span><div id='+$scope.count+' style=width:1400px;height:300px></div>')($scope);
$("#space-for-buttons").append(compiledHTML);
$scope.$apply();
$(".static").children().each(function() {
$(this).on("change", function(){
var id = $(this).closest("span").attr("id");
var chartId = id.slice(-1);
queryService.testing($scope.current_mount1, $scope.current_dedupe1, function(response){
var watever = response.hits.hits;
dataToBePlot = chartOptions.calcParams(watever, totalIops, totalBandwidth, $scope.y_axis_param1);
chartOptions.creatingGraph(dataToBePlot, $scope.y_axis_param1, chartId);
});
});
});
});
}
}
});
Code explanation:
This is just the directive which I am posting.I am appending my compiledHTML and doing $scope.apply to set the select tags to their default values. Whenever any of the select tags are changed I am doing a set of operations (function calls to services) on the values selected.
As you can see the ng-model attribute being attached is the same. So when one select tag is changed the value is reflected on all the appended HTML even though the data displayed does not match to it.
Hope this PLunker is useful for you. You need to have one way binding over such attributes
<p>Hello {{name}}!</p>
<input ng-model="name"/>
<br>Single way binding: {{::name}}
Let me know if I misunderstood your question
It is a bit hard to understand your whole requirement from your description and your code, correct me if I'm wrong: you are trying to dynamically add a dropdown on a button click and then trying to keep track on each of them.
If you are giving the same ng-model for each generated items, then they are bound to the same object, and their behavior is synchronized, that is how angular works.
What you can do is, change your structure to an array, and then assigning ng-model to the elements, so you can conveniently keep track on each of them. I understand you came from jquery base on your code, so let me show you the angular way of doing things.
angular.module('test', []).controller('Test', Test);
function Test($scope) {
$scope.itemArray = [
{ id: 1, selected: "op1" },
{ id: 2, selected: "op2" }
];
$scope.optionList = [
{ name: "Option 1", value: "op1" },
{ name: "Option 2", value: "op2" },
{ name: "Option 3", value: "op3" }
]
$scope.addItem = function() {
var newItem = { id: $scope.itemArray.length + 1, selected: "" };
$scope.itemArray.push(newItem);
}
$scope.changeItem = function(item) {
alert("changed item " + item.id + " to " + item.selected);
}
}
select {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
<div ng-app='test' ng-controller='Test'>
<button type='button' ng-click='addItem()'>Add</button>
<select ng-repeat='item in itemArray'
ng-options='option.value as option.name for option in optionList'
ng-model='item.selected'
ng-change='changeItem(item)'></select>
</div>

How to add dynamic directive names from JSON with the $interpolate function

I'm trying to dynamically add directive names to my directive from a json object. Angular however is only interpolating the directive name which is pulled from a JSON tree once, Angular is then not recognizing and compiling the dynamic children directives once the name is interpolated.
I have tried adding the interpolate service to my DDO so that I can manually interpolate the JSON values, and then have Angular compile.
I however get undefined for $interpolate(tAttrs.$attr.layout) I'm passing the json object to my isolated scope as layout, when I try to access the attr layout I get undefined. My question is how can I access layout object values in the pre link or before compile so that I can interpolate the values and inject them in.
Or do I need to have angular recompile as described here: How do I pass multiple attributes into an Angular.js attribute directive?
Any help would be great.
{
"containers": [
{
"fluid": true,
"rows": [
{
"columns": [
{
"class": "col-md-12",
"directive": "blog"
}
]
},
{
"columns": [
{
"class": "col-md-6 col-md-offset-3 col-xs-10 col-xs-offset-1",
"directive": "tire-finder"
}
]
}
]
}
]
}
...
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<{{column.directive}}></{{column.directive}}>
</div>
</div>
</div>
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate) {
var controller = function(brainService, $scope, $state) {
$scope.directive = {};
var currentState = $state.current.name;
brainService.getDirectiveScope('wpMain', {}).then(function(response) {
$scope.layout = response.states[currentState];
});
};
var compile = function(tElement, tAttrs, transclude) {
var directiveNames = $interpolate(tAttrs.$attr.layout);
}
return {
restrict: 'E',
// replace: true,
scope: {
layout: '=',
},
controller: controller,
templateUrl: 'directive/wpMain/wpMain.html',
compile: compile
};
};
If you're only dealing with a couple options for what a column might be, I would suggest going with #georgeawg's answer.
However, if you expect that number to grow, what you might opt for instead is something along the following lines:
<div layout="layout" ng-repeat="container in layout.containers" ng-class="container">
<div ng-repeat="row in container.rows">
<div ng-repeat="column in row.columns" ng-class="column.class">
<column-directive type="column.directive"></column-directive>
</div>
</div>
and then in your JS...
yourApp.directive('columnDirective', columnDirectiveFactory);
columnDirectiveFactory.$inject = ['$compile'];
function columnDirectiveFactory ($compile) {
return {
restrict: 'E',
scope: {
type: '='
},
link: function (scope, elem, attrs) {
var newContents = $compile('<' + scope.type + '></' + scope.type + '>')(scope);
elem.contents(newContents);
}
};
}
To the best of my knowledge, Angular doesn't have any built-in facility to choose directives in a truly dynamic fashion. The solution above allows you to pass information about which directive you want into a generic columnDirective, whose link function then goes about the business of constructing the correct element, compiling it against the current scope, and inserting into the DOM.
There was an issue with the promise in my original posted code which was preventing me from recompiling the template with the correct directive names. The issue was that I was trying to access the JSON object in the preLink function, but the promise hadn't been resolved yet. This meant that my scope property didn't yet have data.
To fix this I added my service promise to the directive scope $scope.layoutPromise = brainService.getDirectiveScope('wpMain', {}); to which I then called and resolved in my link function. I managed to have Angular compile all of my directive names from the JSON object, but I had to do it in a very hackish way. I will be taking your recommendations #cmw in order to make my code simpler and more 'Angulary'
This is currently my working code:
...
angular.module('rpmsol').directive('wpMain', wpMainDirective);
function wpMainDirective($interpolate, $compile) {
var controller = function(brainService, $scope, $state) {
$scope.currentState = $state.current.name;
$scope.layoutPromise = brainService.getDirectiveScope('wpMain', {});
};
var link = function(scope, element, attributes) {
scope.layoutPromise.then(function sucess(response) {
var template = [];
angular.forEach(response.states[scope.currentState].containers, function(container, containerKey) {
template.push('<div class="container' + (container.fluid?'-fluid':'') + '">');
//loop rows
angular.forEach(container.rows, function(row, rowkey) {
template.push('<div class="row">');
angular.forEach(row.columns, function(column, columnKey) {
template.push('<div class="' + column.class + '">');
template.push('<' + column.directive +'></' + column.directive + '>')
template.push('</div>');
});
template.push('</div>');
});
template.push('</div>');
});
template = template.join('');
element.append($compile(template)(scope));
})
};
return {
scope: true,
controller: controller,
link: link
};
};

click selectively ng-if directive in angular js

HTML :
<div ng-app="myApp" ng-controller="someController as Ctrl">
<div class="clickme" ng-repeat="elems in Ctrl.elem" ng-click="Ctrl.click(elems.title)">
{{elems.title}}
<span>click me</span>
<div id="container">
<test-Input title="elems.title" data="elems.id" ng-if="Ctrl.myId==" >/test-Input>
</div>
</div>
JS :
var Elems = [
{
title : "First",
id : 1
},
{
title : "Second",
id : 2
},
{
title : "Third",
id : 3
}
];
var myApp = angular.module('myApp', []);
myApp.controller('someController', function($scope) {
var self = this;
self.elem = Elems;
self.myId = false;
self.click = function(data){
self.myId = data;
};
});
myApp.directive('testInput',function(){
return {
restrict: 'E',
scope: {
myTitle: '=title',
myId: '=data'
},
template: '<div>{{myTitle}}</div>',
controller: function($scope) {
}
};
});
I'm new to angular js. when I click the "click me" div then I want to make ng-if = true result. then show (not ng-show it will renders every elements) the directive. is there any ways to do it angular way?
Here is a fiddle: http://jsfiddle.net/4L6qbpoy/5/
You can use something like:
<test-Input title="elems.title" data="elems.id" ng-if="elems.isVisible"></test-Input>
and toggle that on click
Check out this jsfiddle
You need to have the ng-if evaluate to true within the ng-repeat. So you need a condition that evaluates the unique value for each object in the array.
ng-if="Ctrl.myId==elems.title"
To understand, each instance of ng-repeat falls under the controller's scope. That means the ng-repeated elements are pushed to the boundaries and are only evaluated within your template. Within those bounds, you have access to a tiny local scope which includes $index, $first, $odd, etc..
You can read more here: https://docs.angularjs.org/api/ng/directive/ngRepeat

ngModel not updating in select

I have a directive rendering inputs based on a configuration sent by the server. Everything is working great except for the 'select' input.
No matter what I try, the ng-model does not update.
I have trimmed my code a lot to isolate the problem :
Javascript :
var myApp = angular.module('example', []);
myApp.factory('DynamicData', [function() {
return {
data: {
backup_frequency: 604800
}
};
}])
.directive('dynamicInput',
['$compile', 'DynamicData', function($compile, DynamicData) {
/**
* Render the input
*/
var render = function render() {
var input = angular.element('<select class="form-control" ng-model="inner.backup_frequency" ng-options="option.value as option.title for option in options"></select>');
return input;
};
var getInput = function ()
{
var input = render();
return input ? input[0].outerHTML : '';
};
var getTemplate = function(){
var template = '<div class="form-group">' +
'Select input ' +
'<div class="col-md-7">' + getInput() + '</div>' +
'</div>';
return template;
};
return {
restrict : 'E',
scope: {
content:'=content',
},
link : function(scope, element, attrs) {
var template = getTemplate();
scope.options = [
{title: "Daily", value: 86400},
{title: "Weekly", value: 604800},
{title: "Monthly", value: 2678400},
];
scope.inner = DynamicData.data;
console.info('inner data', scope.inner);
element.html(template);
element.replaceWith($compile(element.contents())(scope));
}
};
}])
.controller('FormCtrl', ['DynamicData', '$scope', function (DynamicData, $scope){
$scope.app = {};
$scope.save = function save() {
$scope.value = DynamicData.data.backup_frequency;
console.info('DynamicData', DynamicData.data);
};
}]);
HTML :
<head>
<script data-require="angular.js#1.3.8" data-semver="1.3.8" src="https://code.angularjs.org/1.3.8/angular.js"></script>
<link href="style.css" rel="stylesheet" />
<script src="script.js"></script>
</head>
<body>
<h1>Dynamic Input :</h1>
<div data-ng-controller="FormCtrl">
<dynamic-input class="form-group" content="app"></dynamic-input>
<span data-ng-bind="value"></span><br/>
<button class="btn btn-primary" ng-click="save()">Save</button>
</div>
</body>
</html>
A working plunker is available : http://plnkr.co/edit/mNBTJzZXjX6mLyPz6NCI?p=preview
Do you have any ideas why the ng-model in the select is not updated ?
EDIT : What I want to achieve is updating the variable "inner.backup_frequency" (the reference to my data object returned by my factory DynamicData). As you can see in the plunker, whenever I change the option in the select, the variable contained in the ng-model is not updated.
Thank you for your time.
I had a similar issue and find if you directly bind ng-model to a scope variable as below, the scope variable selection somehow will not be updated.
<select ng-model="selection" ng-options="option for option in options">
Instead, define a view model in your scope and bind ng-model to a field of view model the view model field will be updated.
<select ng-model="viewmodel.selection" ng-options="option for option in options">
In your controller, you should do this:
app.controller('SomeCtrl', ['$scope'
function($scope)
{
$scope.viewmodel = {};
});
I've fixed it. What you needed to do was not to replace the element's content with the compiled content, but rather replace it with the raw HTML and only then compile it.
element.html(template);
$compile(element.contents())(scope);
Working Plunker.
EDIT: I've edited your code to work without the directive:
Plunker without directive.
EDIT 2: Also a version where it works with the directive, but without the compile:
Plunker.

Custom directive: How evaluate bindings with dynamic HTML

Setup:
Very simplified HTML:
<td ng-repeat="col in cols">
<div ng-bind-html="col.safeHTML"></div>
</td>
JS controller:
$scope.cols = [
{
field : 'logo',
displayName : 'Logo',
cellTemplate: '<div style="color:red">{{col}}</div>'
},
{
field : 'color',
displayName : 'Color',
cellTemplate: '<div style="color:green">{{col}}</div>
}
];
JS link directive link function:
for (var i = 0, j = $scope.cols.length;
i < j;
i++) {
if ($scope.cols[i].hasOwnProperty('cellTemplate')) {
$scope.cols[i].safeHTML = $sce.trustAsHtml($scope.cols[i].cellTemplate);
}
}
And it is escaping correctly the HTML but the bindings ({{some_var}}) are not being interpolated.
How can make Angular compute the bindings in the safe HTML? I tried to use several variations of bind like ngBindTemplate but was for no use :(
You actually want to use the $compile service if you plan to dynamically compile angular components and add them to the DOM manually.
With a little bit of custom directive work, you can make this work pretty easily.
function compileDirective($compile) {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
//Watch for changes to expression
scope.$watch(attrs.compile, function(newVal) {
//Compile creates a linking function
// that can be used with any scope
var link = $compile(newVal);
//Executing the linking function
// creates a new element
var newElem = link(scope);
//Which we can then append to our DOM element
elem.append(newElem);
});
}
};
}
function colsController() {
this.cols = [{
name: "I'm using an H1",
template: "<h1>{{col.name}}</h1>"
}, {
name: "I'm using an RED SPAN",
template: "<span style=\"color:red\">{{col.name}}</span>"
}];
}
angular.module('sample', [])
.directive('compile', compileDirective)
.controller('colsCtrl', colsController);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0-beta.4/angular.min.js"></script>
<div ng-app="sample">
<ul ng-controller="colsCtrl as ctrl">
<li ng-repeat="col in ctrl.cols">
<!-- The "compile" attribute is our custom directive -->
<div compile="col.template"></div>
</li>
</ul>
</div>

Categories

Resources