Dynamically including directive in an AngularJS partial - javascript

I have a list of directives (normally form fields and custom form controls). Now I will get the list of directive names from backend to use to build a form.
It basically creates a dynamic form where I don't know what all form fields are in the form (it depends on the JSON config file I get from the backend).
Sample JSON:
field1 : {
type: 'text',
directive : 'directive1'
},
field2: {
type : 'dropdown',
directive : 'dropdown-directive'
}
Can I do something similar in AngularJS, and if possible, how?

Use the $compile service against the scope. This will allow you to compile angular code which can be appended to a container.
See jsfiddle: http://jsfiddle.net/p8jjZ/1/
HTML:
<div ng-app="myApp" ng-controller="MainController">
<div custom-elements="myData.elements"></div>
<p>{{user}}</p>
</div>
JavaScript:
var mod = angular.module("myApp", []);
mod.controller("MainController", function ($scope) {
$scope.myData = {};
$scope.myData.elements = {
field1 :{ type: 'text', directive : 'directive1' },
field2: { type : 'dropdown', directive : 'dropdown-directive' }
};
});
mod.directive("customElements", function ($compile) {
return {
restrict: "A",
scope: {
customElements: "="
},
link: function (scope, element, attrs) {
var prop,
elems = scope.customElements,
currElem,
compiled;
for (prop in elems) {
currElem = elems[prop];
console.log("Working on " + prop);
//compile input against parent scope. Assuming directives are attributes, but adapt to your scenario:
compiled = $compile('<div ' + currElem.directive + '></div>')(scope.$parent);
//append this to customElements
element.append(compiled);
}
}
}
});
mod.directive("directive1", function () {
return {
restrict: "A",
template: '<div>Whoa! I am directive 1<br><input type="text" ng-model="user.name"></div>'
}
});
mod.directive("dropdownDirective", function () {
return {
restrict: "A",
template: '<div>I am another directive<br><select ng-model="user.color"><option value="blue">blue</option><option value="green">Green</option></div>'
}
});
The customElement directive just creates the directive as if it were an attribute on an element. This is a very simple example, but should get you started on what you are looking to do where you can update the logic that builds the elements/directive accordingly.

Related

AngularJS: Wrapping Directives and passing the attributes

I am looking for a way to extend or wrap a third-party directives html with Angular 1.5.
Given a directive
<lib-input></lib-input>
I want to create a directive <my-lib-input> which renders the following HTML:
<div>
<my-directive></my-directive>
<lib-input ng-if="vodoo()"></lib-input>
</div>
Which is supposed to be used in the same way as the original directive.
Example
To use my directive in the same way as the original, i need to move all attributes to a specific node of my template:
<my-lib-input ng-model="model" ng-change="ctrl.onChange()"></my-lib-input>
should generate:
<div>
<my-directive></my-directive>
<lib-input ng-if="vodoo()" ng-model="model" ng-change="ctrl.onChange()"></lib-input>
</div>
However angular applies all the attributes to the root-node (here: the div) by default.
Question
How do I apply all parameters/ attributes which are passed to my directive to a specific node of the template?
I would like to prevent hardcoding a list of available parameters in my directive like:
restrict: 'E',
scope : {
ngModel: '=',
ngChange: '&',
...
}
You can have chaining of scope parameters like
Working JSFiddle here
var myApp = angular.module('myApp',[]);
myApp.controller('myCtrl', function($scope) {
$scope.input = 'LibInput';
$scope.changeInput2 = function(i2) {
$scope.myInputs.setInput2(i2);
}
//this is releaving module which have getters and setter and variables can be hidden from outside scope.
var getInputData = function() {
var input1 = 'Input1';
var input2 = 'Input2';
return {
getInput1 : function() {
return input1;
},
getInput2 : function() {
return input2;
},
setInput1 : function(i1) {
input1 = i1;
},
setInput2 : function(i2) {
input2 = i2;
}
}
}
$scope.myInputs = getInputData();
});
myApp.directive('libInput', function() {
return {
restrict : 'E',
scope : {
input : '='
},
template : '<div>{{input}}</div>'
}
});
myApp.directive('myLibInput', function() {
return {
restrict : 'E',
scope : {
input : '=',
myDirInput : '='
},
template : '<my-dir other-input="myDirInput"></my-dir>\
<lib-input input="input"><lib-input>'
}
});
myApp.directive('myDir', function() {
return {
restrict : 'E',
scope : {
otherInput : '='
},
template : '<div>{{otherInput.getInput1()}}</div>\
<div>{{otherInput.getInput2()}}</div>'
}
});

passing complex data to ng-directive

i'm trying to build a custom directive in angular; it needs to rielaborate data passed to it before rendering the page, so in the i need to get data passed to my directive through attributes and do some stuff, and finally render the page.
.directive('lpcEdiTable', function($interpolate) {
return {
restrict: "E",
templateUrl: "...",
replace: false,
scope: {
collection: "="
},
link: function(scope, elem, attr) {
//here i need to retrieve data
var myColl = scope.collection; //it's not working
//do some stuff here on myColl
scope.collection = myColl;
}
};
});
so here's how i use the directive:
<lpc-edi-table collection="products"></lpc-edi-table>
where products is a complex object.
in directive template i use the post elaboration data into ng-repeat and other stuff
i tried to follow this but i could not retrieve data into link function
Here is an example of passing an object to a directive
angular.module("myModule", [])
.controller("baseController", ['$scope', function($scope) {
$scope.products = [
"asd",
"asdasd"
];
}])
.directive('myDirective', function() {
return {
restrict: "E",
template: "<p ng-repeat='item in collection'>{{item.attr}}</p>",
scope: {
collection: "="
},
link: function(scope, elem, attr) {
if (!attr.collection) throw new Error("lpc-edi-table directive: 'collection' attribute not found!");
scope.collection = scope.collection.map(function(a) { return {attr: a} });
console.log(scope.collection);
}
};
});
You can call your directive like
<my-directive collection="products"></my-directive>
DEMO https://plnkr.co/edit/cj4oSPRiNo8iYfinIztT?p=preview
For Angular 1.6, I recommend using components. Especially if you don't need to do advanced DOM manipulation.
app.component('lpcEdiTable', {
// $ctrl is controller instance
template: '<div ng-repeat="object in collection">{{object | json}}</div>',
bindings: { //custom attributes
collection: '<' //one way binding. Can also be two way =
},
controller: function(){
//$onInit gets called when the bindings are ready
this.$onInit = function(){
// this.collection is now ready
// safe to manipulate
};
}
});
This can be used like:
<lpc-edi-table collection="products"></lpc-edi-table>

Pass the value to compile function in directive

I am trying to make some generic field and use directive for that. For example, in HTML code I am defining:
<div def-field="name"></div>
<div def-field="surname"></div>
<div def-field="children"></div>
This field can be two types: either the simple element(as the first two) or a list of elements(as the third one). The scope variable contains the definition of all fields and their types.
For that I created the directive "def-field":
app.directive("defField", function($compile, $parse, $http) {
restrict: 'A', // only for attributes
scope : true,
return {
restrict: 'A', // only for attributes
scope : true,
compile: function compile(tElement, tAttributes) {
//here I need to detect, which type of field is it.
//if it is array, I need to execute the compile code
if(fieldType === 'array') {
//execute secial code for compile furnction
}
}
if(fieldType === 'array') {
//return for array
var returnValue = {pre : linkFunction};
} else {
//return for normal type
var returnValue = {
pre : linkFunction,
post: function(scope, element, attrs){
$compile(element.parent())(scope);
}
};
}
return returnValue;
}
The problem is that I need to get the fieldType from the scope variable and the scope variable is not available in the compile function. Is there is some possibility to workaround this issue?
Currently, I pass as an attribute the type "array", but for this is not an acceptable option.
After reading some material about Angular, I have managed to find the solution. Unfortunately, in my application, the majority of business logic was in controllers, which is wrong according to style guides:
Angular 1 Style Guide by John Papa (business logic)
Angular 1 Style Guide by Todd Motto (business logic)
Therefore, I moved my business logic to controllers and then I was able to retrieve the required data in directive from service.
To show that, I have prepared a small demo example:
Link to Plunker
Explanation of code:
First, I defined a service, which should retrieve the required data:
(function () {
"use strict";
angular.module("dirExampleApp").service("directiveService", ["$timeout", function ($timeout) {
var self = this;
self.getObjectData = function () {
return $timeout(function () {
var responseFromServer = {
firstName: {
value: "firstValue"
},
secondName: {
value: "secondValue"
},
thirdName: {
value: "thirdValue"
},
fourthName: {
value: "fourthValue"
}
};
self.content = responseFromServer;
}, 300);
};
}]);
})();
Then, I can inject this service and use it in my directive in either compile or prelink or postlink functions:
(function () {
"use strict";
angular.module("dirExampleApp").directive("dirExample", ["$log", "directiveService", function ($log, directiveService) {
return {
restrict: "A",
template: "<h3>Directive example!</h3>",
compile: function (tElem, tAttrs) {
var fieldName = tAttrs.dirExample;
$log.log('Compile function: Field with name: ' + fieldName +
' and sevice provided the following data: ' +
directiveService.content[fieldName].value);
return {
pre: function (scope, iElem, iAttrs) {
var fieldName = iAttrs.dirExample;
$log.log('Prelink function: Field with name: ' + fieldName +
' and sevice provided the following data: ' +
directiveService.content[fieldName].value);
},
post: function (scope, iElem, iAttrs) {
var fieldName = iAttrs.dirExample;
$log.log('Postlink function: Field with name: ' + fieldName +
' and sevice provided the following data: ' +
directiveService.content[fieldName].value);
}
};
}
};
}]);
})();
As a result, there is some logging, when the directives are created. This logging demonstarates, that the required data has benn succesfully retrieved from service in compile, prelink and postlink functions of directive:
Please note: I am not sure, whether it is OK to use service, factory or provider for purpose of providing data. I only showed how it is possible with service. I guess, with factory and provider, the logic is the same.

AngularJS Converting Model Value

I'm using Formly to create my input pages and currently have a simple testing page setup where I can test any types or wrappers I'm creating.
It's currently defined as,
Html:
<div data-ng-controller="npTestingCtrl as vm" style="height:100%">
<formly-form form="vm.form" model="vm.model" fields="[
{
key: 'things',
type: 'checkedListBox',
}]"></formly-form>
</div>
Controller:
(function () {
'use strict';
angular.module('app.testing').controller('npTestingCtrl', npTestingCtrl);
npTestingCtrl.$inject = [];
function npTestingCtrl() {
var vm = this;
vm.model = {
things: { }
}
}
})();
I've then declared a "checkedListBox" type as the following:
Type:
angular.module('app.formly.checkedListBox', ['formly'])
.run(function (formlyConfig) {
formlyConfig.setType({
name: 'checkedListBox',
template: '<np-checked-list-box></np-checked-list-box>'
});
});
The directive 'np-checked-list-box' is then declared as:
Directive:
angular.module('app.formly.checkedListBox').directive('npCheckedListBox', function () {
return {
restrict: 'E',
scope: true,
templateUrl: 'checkedListBox.html',
link: function (scope, element, attr) {
scope.items = [{ identifier: 13, text: 'Tom' }, { identifier: 57, text: 'Dick' }, { identifier: 4, text: 'Harry' }];
}
}
});
Directive Html:
<div style="overflow-y:auto;height:{{to.height == undefinied ? 350 : to.height}}px">
<div class="checkbox" ng-repeat="item in items">
<input id="{{options.id}}_{{item.identifier}}"
type="checkbox"
ng-model="model[options.key][item.identifier]"
value="{{item.checked}}">
<label for="{{options.id}}_{{item.identifier}}">{{item.text}}</label>
</div>
</div>
This is working correctly, in so much as when I click on any of the checkboxes a property is added to the things object in my model with either true or false as a value, e.g.
things: {
13: true,
57: false
}
I would now like to convert the things object into an array which stores only the items which are true. E.g. I want to end up with an array of identifiers I can post to the server.
As this type will be used in multiple places I only want to have the conversion logic once, e.g. in the directive so have tried changing my Formly template to:
<np-checked-list-box ng-Model="model[options.key]"></np-checked-list-box>
I then injected the ngModelCtrl into the directive, adding a function to both the $formatters and $parsers. This doesn't work however as the functions are never called so I can't manipulate the values. I assume this is because the object it's self isn't changed, it's just has properties add or within it changed.
Is what I'm trying to do possible and if so what do I need to change to make it work?
If it's not possible is there a way to change my model bindings to do as I've described?
FYI for anyone who comes across this in the end I done the following:
Modified the model of my testing controller to:
vm.model = {
things: []
}
E.g. turnings things from an object into an array.
Modified the HTML of the 'np-checked-list-box' directive to:
<div style="overflow-y:auto;height:{{to.height == undefinied ? 350 : to.height}}px">
<div class="checkbox" ng-repeat="item in items">
<input id="{{options.id}}_{{item.identifier}}"
type="checkbox"
value="{{item.checked}}"
data-ng-model="model[options.key]"
data-np-checked-list-box-item
data-np-checked-list-box-item-identifier="{{item.identifier}}">
<label for="{{options.id}}_{{item.identifier}}">{{item.text}}</label>
</div>
</div>
Notice the data-ng-model="model[options.key]" is binding directly to the array rather than an element in that array. I have also added another directive 'data-np-checked-list-box-item' and an attribute 'data-np-checked-list-box-item-identifier' it will use.
Created a new directive 'data-np-checked-list-box-item':
angular.module('app.formly.checkedListBox').directive('npCheckedListBoxItem', function () {
return {
require: 'ngModel',
restrict: 'A',
scope: {
identifier: "#npCheckedListBoxItemIdentifier"
},
link: function (scope, element, attr, ngModelCtrl) {
var model = [];
ngModelCtrl.$formatters.push(function (modelValue) {
model = modelValue;
return model[scope.identifier] == true;
});
ngModelCtrl.$parsers.push(function (viewValue) {
if (viewValue) {
model.push(scope.identifier);
} else {
for (var i = model.length - 1; i >= 0; i--) {
if (model[i] == scope.identifier) {
model.splice(i, 1);
break;
}
}
}
return model;
});
}
}
});
The directive is just a wrapper around the array which on a per identifier basis (e.g. for each checkbox) will return 'true' if there is a matching identifier in the array or otherwise false.
When updating the model it will add the identifier to the array when the checkbox is checked or remove it when unchecked.
E.g. if the checkboxes for "Tom" and "Dick" where checked, my model would look like.
model: {
things: ["13", "57"]
}
It is storing the entries as strings but for my purposes this is fine.

Angular Directive refresh on parameter change

I have an angular directive which is initialized like so:
<conversation style="height:300px" type="convo" type-id="{{some_prop}}"></conversation>
I'd like it to be smart enough to refresh the directive when $scope.some_prop changes, as that implies it should show completely different content.
I have tested it as it is and nothing happens, the linking function doesn't even get called when $scope.some_prop changes. Is there a way to make this happen ?
Link function only gets called once, so it would not directly do what you are expecting. You need to use angular $watch to watch a model variable.
This watch needs to be setup in the link function.
If you use isolated scope for directive then the scope would be
scope :{typeId:'#' }
In your link function then you add a watch like
link: function(scope, element, attrs) {
scope.$watch("typeId",function(newValue,oldValue) {
//This gets called when data changes.
});
}
If you are not using isolated scope use watch on some_prop
What you're trying to do is to monitor the property of attribute in directive. You can watch the property of attribute changes using $observe() as follows:
angular.module('myApp').directive('conversation', function() {
return {
restrict: 'E',
replace: true,
compile: function(tElement, attr) {
attr.$observe('typeId', function(data) {
console.log("Updated data ", data);
}, true);
}
};
});
Keep in mind that I used the 'compile' function in the directive here because you haven't mentioned if you have any models and whether this is performance sensitive.
If you have models, you need to change the 'compile' function to 'link' or use 'controller' and to monitor the property of a model changes, you should use $watch(), and take of the angular {{}} brackets from the property, example:
<conversation style="height:300px" type="convo" type-id="some_prop"></conversation>
And in the directive:
angular.module('myApp').directive('conversation', function() {
return {
scope: {
typeId: '=',
},
link: function(scope, elm, attr) {
scope.$watch('typeId', function(newValue, oldValue) {
if (newValue !== oldValue) {
// You actions here
console.log("I got the new value! ", newValue);
}
}, true);
}
};
});
I hope this will help reloading/refreshing directive on value from parent scope
<html>
<head>
<!-- version 1.4.5 -->
<script src="angular.js"></script>
</head>
<body ng-app="app" ng-controller="Ctrl">
<my-test reload-on="update"></my-test><br>
<button ng-click="update = update+1;">update {{update}}</button>
</body>
<script>
var app = angular.module('app', [])
app.controller('Ctrl', function($scope) {
$scope.update = 0;
});
app.directive('myTest', function() {
return {
restrict: 'AE',
scope: {
reloadOn: '='
},
controller: function($scope) {
$scope.$watch('reloadOn', function(newVal, oldVal) {
// all directive code here
console.log("Reloaded successfully......" + $scope.reloadOn);
});
},
template: '<span> {{reloadOn}} </span>'
}
});
</script>
</html>
angular.module('app').directive('conversation', function() {
return {
restrict: 'E',
link: function ($scope, $elm, $attr) {
$scope.$watch("some_prop", function (newValue, oldValue) {
var typeId = $attr.type-id;
// Your logic.
});
}
};
}
If You're under AngularJS 1.5.3 or newer, You should consider to move to components instead of directives.
Those works very similar to directives but with some very useful additional feautures, such as $onChanges(changesObj), one of the lifecycle hook, that will be called whenever one-way bindings are updated.
app.component('conversation ', {
bindings: {
type: '#',
typeId: '='
},
controller: function() {
this.$onChanges = function(changes) {
// check if your specific property has changed
// that because $onChanges is fired whenever each property is changed from you parent ctrl
if(!!changes.typeId){
refreshYourComponent();
}
};
},
templateUrl: 'conversation .html'
});
Here's the docs for deepen into components.

Categories

Resources