When creating a directive, while defining isolate scope with two way binding using = is there any way that I can bind an array of scope variables. ie. if in my controller I have objects defined like $scope.one, $scope.two etc. and there can be any number of those - I want the directive to be able to handle a configurable number of them. How could I do that?
I can't do this, since another controller that uses the directive may have ten, so I want it to be flexible:
.directive("example", function () {
return {
scope: {
one: "=",
two: "=",
three: "="
},
...
Off course it is:
.directive('example', function() {
return {
scope: {
config: '='
},
link: function(scope) {
var firstOption = scope.config[0];
var secondOption = scope.config[1];
//...
}
}
}
The array options would have to be stored at a fixed index, so it would be less readable than passing a config object
.directive('example', function() {
return {
scope: {
config: '='
},
link: function(scope) {
var firstOption = scope.config.firstOption;
var secondOption = scope.config.secondOption;
//...
}
}
}
Related
I have a directive with isolated scope. I am modifying one of the variables passed from the parent controller in the controller of the directive. The issue I'm running into is that when I use multiple instances of this directive (with different options and model) on the same view, the options object does not remain unique to each instance of the directive. Instead, it becomes a shared variable and all the instances of the directive use the same options object.
So if I had used them in my view like below, with optionsA.isFlagOn = true and optionsB.isFlagOn = false
<my-directive model="modelA" options="optionsA">
<my-directive model="modelB" options="optionsB">
Directive with modelB loads with the optionsA.
How do I keep options unique while modifying it for each specific instance?
angular.module('myModule', [])
.directive('myDirective', function($compile) {
template = '<h3><span ng-bind="model.title"><h3><p><span ng-bind="options"></span></p>';
return {
restrict: 'AE',
scope: {
model: "=",
options: "=?" //A JSON object
},
controller: function($scope) {
$scope.options = $scope.options || {};
//A function that sets default values if no options object passed
ensureDefaultOptions($scope);
//now based on some of the options passed in, I modify a property in the options object
if ($scope.options.isFlagOn)
$scope.options.thisProp = true;
},
link: function(scope, element, attr) {
let content = $compile(template)(scope);
element.append(content);
}
};
}
Edit: I solved my issue. My solution is posted in the answer below.
Can you change it to you one-way bindings with:
scope: {
model: "=",
options: "<?" //A JSON object
}
Your directive should make a copy of passed in options combined with defaults, so each directive instance have its own option object.
You can achieve that easily by using extend
var defaultOptions = { a:1, b:2, c:3 };
var options = angular.extend(defaultOption, $scope.options);
// then use options everywhere
Note that this will only be done once during init, so if your options come from controller asynchronously, you'll need extra handling.
I solved it using the bindToController property of Angular directive available in 1.4x or higher.
angular.module('myModule', [])
.directive('myDirective', function($compile) {
template = '<h3><span ng-bind="vm.model.title"><h3><p><span ng-bind="myOptions"></span></p>';
return {
restrict: 'AE',
bindToController: {
model: "=",
options: "=?" //A JSON object
},
scope: {},
controller: function() {
var vm = this;
//a function that handles modifying options
vm.setOptions = function(options){
let newOptions = {};
angular.copy(options, newOptions);
// modify newOptions here
return newOptions;
}
},
controllerAs: 'vm',
link: function(scope, element, attr) {
ensureDefaultOptions(scope.vm);
scope.myOptions = scope.vm.setOptions(scope.vm.options);
let content = $compile(template)(scope);
element.append(content);
}
};
});
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.
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.
I'm trying to set a controller dynamically to my directive using the name property. So far this is my code.
html
<view-edit controller-name="vm.controller" view="home/views/med.search.results.detail.resources.audios.html" edit="home/views/med.media.resources.edit.html"></view-edit>
js
export default class SearchResultsCtrl extends Pageable {
/*#ngInject*/
constructor($injector, $state, api) {
super(
{
injector: $injector,
endpoint: 'mediaMaterialsList',
selectable:{
itemKey: 'cid',
enabled:true,
params: $state.params
},
executeGet: false
}
);
this.controller = SearchResultsResourcesAudiosCtrl;
}
}
Directive
export default class ViewEditDirective {
constructor() {
this.restrict = 'E';
this.replace = true;
this.templateUrl = 'home/views/med.view.edit.html';
this.scope = {};
this.controller = "#";
this.name = "controllerName";
this.bindToController = {
'view': '#?',
'edit': '#?'
};
this.open = false;
this.controllerAs = 'ctrl';
}
}
I get undefined for vm.controller. I guess that it's rendering before the controller can assign the controller to the variable (I debbuged it, and it's setting the controller in the variable).
I'm following this answer to achieve this, but no luck so far.
How to set the dynamic controller for directives?
Thanks.
The problem is not related to ES6 (which is a sugar syntax coating over ES5), this is how Angular scope life cycle works.
This directive may show what's the deal with attribute interpolation
// <div ng-init="a = 1"><div sum="{{ a + 1 }}"></div></div>
app.directive('sum', function () {
return {
scope: {},
controller: function ($attrs) {
console.log($attrs.sum) // {{ a + 1 }}
// ...
},
link: function (scope, element, attrs) {
console.log(attrs.sum) // 2
}
};
});
And $attrs.sum may still not be 2 in link if a value was set after that (i.e. in parent directive link).
It is unsafe (and wrong per se) to assume that the value on one scope can be calculated based on the value from another scopes at some point of time. Because it may be not. That is why watchers and data binding are there.
All that controller: '#' magic value does is getting uninterpolated attribute value and using it as controller name. So no, it won't interpolate controller name from vm.controller and will use 'vm.controller' string as controller name.
An example of a directive that allows to set its controller dynamically may look like
// dynamic-controller="{{ ctrlNameVariable }}"
app.directive('dynamicController', function () {
return {
restrict: 'A',
priority: 2500,
controller: function ($scope, $element, $attrs, $interpolate, $compile) {
var ctrlName = $interpolate($attrs.dynamicController)($scope);
setController(ctrlName);
$attrs.$observe('dynamicController', setController);
function setController (ctrlName) {
if (!ctrlName || $attrs.ngController === ctrlName) {
return;
}
$attrs.$set('ngController', ctrlName);
$compile($element)($scope);
}
}
};
});
with all the side-effects that re-compilation may bring.
I have a specific scenario for a AngularJS directive:
Normally the directive should inherit the default scope
But for some specific scenarios I'd like to replace all values in $scope.myValues with myValues (object loaded from a web-service)
I cannot change in this scenario the main-scope because this is owned by another application (more or less a plugin-mechanism).
Thanks & Regards
Stefan
If think I have found the solution:
Sample Html:
<wi-view data-layout="{{passLayout}}"></wi-view>
<hr />
Original property: {{layout.property1}}
Sample Controller:
app.controller('wiController', function($scope) {
// Simulating the original scope values
$scope.layout = {};
$scope.layout.property1 = 'Original Value';
// New scope values, just here for binding it to the controller
var passLayout = {};
passLayout.property1 = 'Value Overwritten';
passLayout.property2 = 'Another Property';
$scope.passLayout = passLayout;
});
Sample Directive:
app.directive('wiView', function () {
var linkFunction = function(scope, elems, attrs) {
if (attrs.layout !== undefined) {
scope.layout = angular.fromJson(attrs.layout);
}
};
return {
restrict: "E",
scope: true,
priority: 0,
link: linkFunction,
template: '<div>Hello, {{layout.property1}}!</div>'
};
});