AngularJS proxy directive - angularjs

I am trying to create a proxy directive like so:
<x-field x-purpose="choice" x-values="data.countries" ng-model="model.country"/>
Where the field directive forwards this to another directive, causing the following replacement:
<x-choiceField x-values="data.countries" ng-model="model.country"/>
[note:] the ng-model could be replaced by a reference to some new isolated scope.
The "field purpose" directive decides which implementation to use (e.g. drop-down/listbox/autocomplete?) based on how many values there are to choose from, client device size, etc - ultimately resulting in something like this:
<select ng-model="model.country" ng-options="data.countries">
This design is largely out of curiosity rather than for any practical reason, I am interested in how to achieve it rather than whether it is actually a good idea from a performance/simplicity point of view...
After reading [https://stackoverflow.com/a/18895441/1156377], I have something like this:
function proxyDirective($injector, $parse, element) {
return function (scope, element, attrs) {
var target = element.camelCase(attrs.name + '-field');
var model = attrs.ngModel;
var value = $parse(model);
var directive = $injector.get(target);
/* Bind ngModel to new isolated scope "value" property */
scope.$watch(model, function () {
???
});
/* Generate new directive element */
var pElement = angular.element.html('');
var pAttrs = {
value: ???
};
/* Forward to new directive */
return directive.compile(element, attrs, null)(scope, element, attrs);
};
}
function alphaFieldDirective() {
return {
replace: 'true',
template: '<input type="text" ng-value="forwarded value?">'
};
}
function betaFieldDirective() {
return {
replace: 'true',
template: '<textarea attributes? >{{ value }}</textarea>'
};
}
But I'm not sure how to achieve the forwarding or binding. This is my first forage into Angular directives, and it doesn't seem to be a particularly popular way of using them!
The purpose of this is to separate the purpose of a form field from its appearance/implementation, and to provide one simple directive for instantiating fields.

I implemented this via a service which proxies directives:
Fiddle: http://jsfiddle.net/HB7LU/7779/
HTML:
<body ng-app="myApp">
<h1>Directive proxying</h1>
<proxy target="bold" text="Bold text"></proxy>
<h1>Attribute forwarding</h1>
<proxy target="italic" style="color: red;" text="Red, italic text"></proxy>
</body>
Javascript:
angular.module('myApp', [])
.factory('directiveProxyService', directiveProxyService)
.directive('proxy', dirProxy)
.directive('bold', boldDirective)
.directive('italic', italicDirective)
;
function directiveProxyService($compile) {
return function (target, scope, element, attrs, ignoreAttrs) {
var forward = angular.element('<' + target + '/>');
/* Move attributes over */
_(attrs).chain()
.omit(ignoreAttrs || [])
.omit('class', 'id')
.omit(function (val, key) { return key.charAt(0) === '$'; })
.each(function (val, key) {
element.removeAttr(attrs.$attr[key]);
forward.attr(attrs.$attr[key], val);
});
$compile(forward)(scope);
element.append(forward);
return forward;
};
}
function dirProxy(directiveProxyService) {
return {
restrict: 'E',
terminal: true,
priority: 1000000,
replace: true,
template: '<span></span>',
link: function (scope, element, attrs) {
directiveProxyService(attrs.target, scope, element, attrs, ['target']);
}
};
}
function boldDirective() {
return {
restrict: 'E',
replace: true,
template: '<i>{{ text }}</i>',
scope: { text: '#' }
};
}
function italicDirective() {
return {
restrict: 'E',
replace: true,
template: '<i>{{ text }}</i>',
scope: { text: '#' }
};
}

Related

Nested and dynamically created directives in angular.js

I am stuck with this for quite a long time.
In a directive, I would like to create another directive on the fly, based on a function. Instead of having 4 directive declarations, I would prefer to create a new directive in each 'tab' directive, that is to say each time a tab attribute is set in a DOM element.
Here is a part of the code (config is a factory that is use to configure some stuff) :
.directive('tab', function(config) {
return {
require: '^panelHandler',
restrict: 'A',
scope: true,
link: function(scope, elem, attrs, ctrl) {
ctrl.addPane(scope);
scope.select = function() {
ctrl.select(scope);
};
},
};
})
.directive('page1', directiveConfigurer('page1.html'))
.directive('page2', directiveConfigurer('page2.html'))
.directive('page3', directiveConfigurer('page3.html'))
.directive('page4', directiveConfigurer('page4.html'));
function directiveConfigurer(fileName) {
newDirective.$inject = ['config'];
return newDirective;
function newDirective(config) {
var directive = {
restrict: 'E',
scope: true,
templateUrl: config.filesDirectory + fileName,
};
return directive;
}
}
Thanks for your help.
EDIT :
Config...
angular.module('appLogic', ['socket-factory', 'data-factory', 'panelHandler-module'])
.factory('config', function() {
return {
filesDirectory : '../../templates/pages/',
fieldsNumber : 5,
};
});
and what I need...
link: function(scope, elem, attrs, ctrl) {
ctrl.addPane(scope);
//.directive('page' + number, directiveConfigurer(name))
scope.select = function() {
ctrl.select(scope);
};
},
If the directives are essentially the same, except for the template url, then you can just create a single directive and provide the concrete url path as an attribute:
<page src="page1.html">
To do that, use a function for templateUrl property of the directive definition object:
.directive("page", function(){
return {
templateUrl: function(tElem, tAttr){
return "/base/path/" + tAttr.src;
},
//...
};
});

Using an AngularJS directive, how do I pass a custom model from one directive to another?

Background
I am building a custom form building directive. Yes, I am aware that there are many out there already, thanks.
Using ngModel from within my directive, I loop through a JSON array of control objects for my form:
$scope.myForm.controls = [{label: "foo", controlType: "input"}, {label: "bar", controlType: "checkbox"}];
From there, the plan is to $compile in individual directives for each form control.
Here is the primary directive (working fine):
app.directive('myDynamicForm', ['$compile', function ($compile) {
return {
restrict: 'E', // supports using directive as element only
scope: {
ngModel: "="
},
replace: true,
link: function (scope, element, attrs) {
angular.forEach(scope.ngModel, function (model) {
var el = "";
switch (model.controlType.toLowerCase()) {
case "input":
el = "<my-input ng-model='model'></my-input>";
break;
default:
break;
}
$compile(el)(scope);
element.append(el);
});
}
};
}])
Here is the usage:
<my-dynamic-form ng-model="myForm.controls"></my-dynamic-form>
Again, this part works fine.
Problem
As I create those new directive-based elements, I need to pass to them--as their ng-model--the specific object being iterated over in the forEach loop of the prime directive.
So, here is an example control directive:
directive('myInput', ['$compile', function ($compile) {
return {
restrict: 'E',
scope: {
ngModel: "="
},
replace: true,
templateUrl: "/templates/input.html",
link: function (scope, element) {
var obj = scope.ngModel; //<==SHOULD BE SAME AS "model" IN THE FORM BUILDING DIRECTIVE!!
}
};
}])
Usage:
<myInput ng-model="[model from parent form building directive]"></myInput>
Right now, if I set the ng-model of the child directive to <my-input ng-model='ngModel'></my-input>, I get the same collection that the parent directive is iterating.
If I set it to "model" <my-input ng-model='model'></my-input> I get undefined as the value of the child directive.
Any help is appreciated.
try to access child model by index:
app.directive('myDynamicForm', ['$compile', function ($compile) {
return {
restrict: 'E', // supports using directive as element only
scope: {
ngModel: "="
},
replace: true,
link: function (scope, element, attrs) {
angular.forEach(scope.ngModel, function (model, index) {
var el = "";
switch (model.controlType.toLowerCase()) {
case "input":
el = "<my-input ng-model='ngModel[" + index + "]'></my-input>";
break;
default:
break;
}
$compile(el)(scope);
element.append(el);
});
}
};
}])

Angular Isolate Scope breaks down?

I have the following markup:
<div class="controller" ng-controller="mainController">
<input type="text" ng-model="value">
<div class="matches"
positions="{{client.positions | filter:value}}"
select="selectPosition(pos)">
<div class="match"
ng-repeat="match in matches"
ng-click="select({pos: match})"
ng-bind="match.name">
Then, inside my matches directive I have
app.directive('matches', function()
{
return {
scope: {
select: '&'
},
link: function(scope, element, attrs)
{
scope.matches = [];
attrs.$observe('positions', function(value)
{
scope.matches = angular.fromJson(value);
scope.$apply();
})
}
}
}
When I do this, I can console log scope.matches, and it does change with the value from my input. However, the last div .match doesn't render anything! If I remove scope: {...} and replace it with scope: true, then it does render the result, but I want to use the & evaluation to execute a function within my main controller.
What do i do?
Use scope.$watch instead, you can watch the attribute select whenever changes are made from that attribute.
app.directive('matches', function()
{
return {
scope: true,
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch(attrs.select, function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
UPDATE: Likewise, if you define select itself as a scope attribute, you must use the = notation(use the & notation only, if you intend to use it as a callback in a template defined in the directive), and use scope.$watch(), not attr.$observe(). Since attr.$observe() is only used for interpolation changes {{}}, while $watch is used for the changes of the scope property itself.
app.directive('matches', function()
{
return {
scope: {select: '='},
link: function(scope, element, attrs)
{
scope.matches = [];
scope.$watch('select', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
}
The AngularJS Documentation states:
$observe(key, fn);
Observes an interpolated attribute.
The observer function will be invoked once during the next $digest
following compilation. The observer is then invoked whenever the
interpolated value changes.
Not scope properties defined as such in your problem which is defining scope in the directive definition.
If you don't need the isolated scope, you could use $parse instead of the & evaluatation like this:
var selectFn = $parse(attrs.select);
scope.select = function (obj) {
selectFn(scope, obj);
};
Example Plunker: http://plnkr.co/edit/QZy6TQChAw5fEXYtw8wt?p=preview
But if you prefer the isolated scope, you have to transclude children elements and correctly assign the scope of your directive like this:
app.directive('matches', function($parse) {
return {
restrict: 'C',
scope: {
select: '&',
},
transclude: true,
link: function(scope, element, attrs, ctrl, transcludeFn) {
transcludeFn(scope, function (clone) {
element.append(clone);
});
scope.matches = [];
attrs.$observe('positions', function(value) {
scope.matches = angular.fromJson(value);
});
}
}
});
Example Plunker: http://plnkr.co/edit/9SPhTG08uUd440nBxGju?p=preview

Passing id and as a parameter to directive

http://jsfiddle.net/mato75/t48qn/
I have a directive, that if id is not passed, then it should generate one, but it looks like that the generation is to slow and the id is not present in the directive.
(function (angular) {
'use strict';
angular.module('Widgets.Module')
.directive('myDirective', [
function () {
function postLink(scope, jqElm, attr) { }
function postCompile(tElement, tAttrs) {
return function postLink(scope, jqElm, attr) {
attr.$observe("id", function (id) { // called on on init
scope.id = id !== undefined ? id : 'something 1';
});
}
}
function Ctrl(scope) {
}
return {
template:
'<div id="{{ id }}">' +
'</div>',
controller: [
'$scope', Ctrl
],
replace: true,
scope: {
id: '#'
},
restrict: 'AC',
link: postLink,
compile: postCompile
};
}
])
;
})(window.angular)
I think using id is special since its a valid DOM attribute. In my case id was also getting added as an attribute to the directive html, not the inner child where I was using it.
I created a new attribute called input-id that doesn't suffer from this name collision.
<autosuggest input-id="country"></autosuggest>
The produced markup is:
<div class="autosuggest"><input id="country"></div>
...which is what I think you are after.
The scope block for the directive looks like this:
scope: {
inputId: '#'
}
One possible solution is to disable the automatic data binding (scope: {}) and do it manually in your link function.
Check this fiddle.
module.directive('myDialog', function () {
return {
replace: true,
restrict: 'E',
scope: {},
template: '<div>Test {{a1}}</div>',
link: function (scope, element, attrs) {
if (!attrs.a1) {
scope.a1 = "default";
} else {
scope.a1 = attrs.a1;
}
}
}
});

How to hide element if transcluded contents are empty?

I created a very simple directive which displays a key/value pair. I would like to be able to automatically hide the element if the transcluded content is empty (either zero length or just whitespace).
I cannot figure out how to access the content that gets transcluded from within a directive.
app.directive('pair', function($compile) {
return {
replace: true,
restrict: 'E',
scope: {
label: '#'
},
transclude: true,
template: "<div><span>{{label}}</span><span ng-transclude></span></div>"
}
});
For example, I would like the following element to be displayed.
<pair label="My Label">Hi there</pair>
But the next two elements should be hidden because they don't contain any text content.
<pair label="My Label"></pair>
<pair label="My Label"><i></i></pair>
I am new to Angular so there may be a great way handle this sort of thing out of the box. Any help is appreciated.
Here's an approach using ng-show on the template and within compile transcludeFn checking if transcluded html has text length.
If no text length ng-show is set to hide
app.directive('pair', function($timeout) {
return {
replace: true,
restrict: 'E',
scope: {
label: '#'
},
transclude: true,
template: "<div ng-show='1'><span>{{label}} </span><span ng-transclude></span></div>",
compile: function(elem, attrs, transcludeFn) {
transcludeFn(elem, function(clone) {
/* clone is element containing html that will be transcludded*/
var show=clone.text().length?'1':'0'
attrs.ngShow=show;
});
}
}
});
Plunker demo
Maybe a bit late but you can also consider using the CSS Pseudo class :empty.
So, this will work (IE9+)
.trancluded-item:empty {
display: none;
}
The element will still be registered in the dom but will be empty and invisible.
The previously provided answers were helpful but didn't solve my situation perfectly, so I came up with a different solution by creating a separate directive.
Create an attribute-based directive (i.e. restrict: 'A') that simply checks to see if there is any text on all the element's child nodes.
function hideEmpty() {
return {
restrict: 'A',
link: function (scope, element, attr) {
let hasText = false;
// Only checks 1 level deep; can be optimized
element.children().forEach((child) => {
hasText = hasText || !!child.text().trim().length;
});
if (!hasText) {
element.attr('style', 'display: none;');
}
}
};
}
angular
.module('directives.hideEmpty', [])
.directive('hideEmpty', hideEmpty);
If you only want to check the main element:
link: function (scope, element, attr) {
if (!element.text().trim().length) {
element.attr('style', 'display: none;');
}
}
To solve my problem, all I needed was to check if there were any child nodes:
link: function (scope, element, attr) {
if (!element.children().length) {
element.attr('style', 'display: none;');
}
}
YMMV
If you don't want to use ng-show every time, you can create a directive to do it automatically:
.directive('hideEmpty', ['$timeout', function($timeout) {
return {
restrict: 'A',
link: {
post: function (scope, elem, attrs) {
$timeout(function() {
if (!elem.html().trim().length) {
elem.hide();
}
});
}
}
};
}]);
Then you can apply it on any element. In your case it would be:
<span hide-empty>{{label}}</span>
I am not terribly familiar with transclude so not sure if it helps or not.
but one way to check for empty contents inside the directive code is to use iElement.text() or iElement.context object and then hide it.
I did it like this, using controllerAs.
/* inside directive */
controllerAs: "my",
controller: function ($scope, $element, $attrs, $transclude) {
//whatever controller does
},
compile: function(elem, attrs, transcludeFn) {
var self = this;
transcludeFn(elem, function(clone) {
/* clone is element containing html that will be transcluded*/
var showTransclude = clone.text().trim().length ? true : false;
/* I set a property on my controller's prototype indicating whether or not to show the div that is ng-transclude in my template */
self.controller.prototype.showTransclude = showTransclude;
});
}
/* inside template */
<div ng-if="my.showTransclude" ng-transclude class="tilegroup-header-trans"></div>

Resources