i'm trying to create a layer on top of the md-chips called chip-filter
Which has some extra functionality build in;e.g.
a navigation chip (when removed, redirects the user back to a specific url)
add from any place n the app a chip
...
So I got the basics working, hooking in the componentsRegistry, and being able to call it from any place.
But now I'm trying to get the chips from the chipFilterController into the <md-chips>
html:
<chip-filter md-component-id="testId">
<md-chips ng-model="chips">
<md-chip-template>
<strong>{{$chip}}</strong>
<em>(type)</em>
</md-chip-template>
</md-chips>
</chip-filter>
and directive:
function chipFilterDirective($log) {
function postLink(scope, element, attr, sidenavCtrl) {
element.on("$destroy", function() {
sidenavCtrl.destroy();
});
}
return {
restrict: "E",
scope: {},
controller: "chipFilterController",
compile: function(element) {
return postLink;
}
};
}
and controller:
function chipFilterController($scope, $element, $attrs, $mdComponentRegistry, $q, $log) {
var self = this;
$scope.chips = [];
function addChip(input, type) {
var def = $q.defer();
$scope.chips.push({
name: input,
type: type
});
def.resolve();
return def.promise;
}
self.addNavigationChip = function() {
return addChip("stuff");
};
self.addChip = function() {
return addChip("stuff");
};
self.destroy = $mdComponentRegistry.register(self, $attrs.mdComponentId);
}
full codepen:
http://codepen.io/cskiwi/pen/eJryqK?editors=1010
setting the scope to false (or removing it from the object) made it work
Related
I are trying to bind a $scope.model.value to a popover.
With all the articles I have something working by storing a copy in the $scope then using a watch to update it if it changes who ever that is not a viable solution as I want to take the popover and use it multiple times with different arrays.
my html on the the main body is:
<button href="#" colorpopover
type="button"
class="btn btn-success btn-rainbow"
data-toggle="popover"
data-trigger="click"
title="Button Color"
id="static-color-popover-{{$id}}"
ng-model="model.value.buttonStatic.buttonColor">
Button Color
</button>
my controller has this code
app.directive('colorpopover', function ($compile, $templateCache, $q, $http) {
var getTemplate = function () {
var def = $q.defer();
var template = '';
template = $templateCache.get('/App_Plugins/IBD.ButtonStyles/Popovers/IBD.Color.Popover.html');
if (typeof template === "undefined") {
$http.get("/App_Plugins/IBD.ButtonStyles/Popovers/IBD.Color.Popover.html").then(function (data) {
$templateCache.put("templateId.html", data);
def.resolve(data);
});
} else {
def.resolve(template);
}
return def.promise;
}
return {
restrict: "A",
link: function (scope, element, attrs, model) {
getTemplate().then(function (popOverContent) {
// Make sure to remove any popover before hand (please confirm the method)
var compileContent = $compile(popOverContent.data)(scope);
var options = {
bindToController: true,
content: compileContent,
placement: "left",
html: true,
date: scope.date, };
$(element).popover(options);
});
}
};
});app.directive('colorpopover', function ($compile, $templateCache, $q, $http) {
var getTemplate = function () {
var def = $q.defer();
var template = '';
template = $templateCache.get('/App_Plugins/IBD.ButtonStyles/Popovers/IBD.Color.Popover.html');
if (typeof template === "undefined") {
$http.get("/App_Plugins/IBD.ButtonStyles/Popovers/IBD.Color.Popover.html").then(function (data) {
$templateCache.put("templateId.html", data);
def.resolve(data);
});
} else {
def.resolve(template);
}
return def.promise;
}
return {
restrict: "A",
link: function (scope, element, attrs, model) {
getTemplate().then(function (popOverContent) {
// Make sure to remove any popover before hand (please confirm the method)
var compileContent = $compile(popOverContent.data)(scope);
var options = {
bindToController: true,
content: compileContent,
placement: "left",
html: true,
date: scope.date, };
$(element).popover(options);
});
}
};
});
the template in basic terms
is
<div class="row">
<div ng-repeat="colorGroup in model.value" ng-class="!$last ? '' : 'last'">
<p>{{colorGroup.val1}}</p>
<p>{{colorGroup.val2}}</p>
<p>{{colorGroup.val3}}</p>
the model structure is
$scope.model.value.buttonStatic.buttonColor[val1,val2,val3]
$scope.model.value.buttonStatic.buttonHover[val1,val2,val3]
$scope.model.value.buttonStatic.buttonFocus[val1,val2,val3]
so eventually I want three buttons as a above with each of the tree values passed.
so at present the ng-repeat is repeating on the model.value of the child scope which is a direct copy of the parent.
the value in the template is going to change on the popover so it needs to go back to the parent.
With much trial and error I have solved the problem.
in the directive I needed to tie add these 2 line before the link.
require: 'ngModel', // ensures the model is passed in
scope: { model: '=ngModel' }, //ties the ng-model to the scope of the popover
then all I needed to do was set the ng-model in my button to model.value......
and in the popover template i just use model
I'm trying to call directive method on button click from the calling controller.
Here is the directive code:
myApp.directive("helloDirective", function() {
return {
restrict: "E",
template: '<input type="text" data-ng-model="model.msg" />',
scope: {},
bindToController: {
param: "="
},
controller: 'helloDirectiveController',
controllerAs: 'model'
}
})
.controller("helloDirectiveController", function() {
var self = this;
self.actions = {
get: function() {
return self.msg;
},
set: function(msgData) {
self.msg = msgData;
}
});
I have call the get and set method from controller..
myApp.controller("indexController", [function() {
var self = this;
self.helloParam ={};
self.get = function() {
//how to call the Directive get method from here
}
}]);
i tried to create a fiddle here
plnkr
The idea
For me, the cleanest solution (so far) for your problem is a solution used by Angular Material developers (I don't know if they were the authors, but I found it there). I've used it once in my project and it worked like a charm.
The idea is to create a global registry for directives' actions. Directives would be stored there by unique ids. We can also create a service dedicated for each directive, just in case we need some external logic.
The solution
1. Components registry
Firstly, we need a components registry. It can be a really simple service:
angular.module('app');
.service('$componentsRegistry', function() {
var self = this;
var components = {};
self.put = function(id, actions) {
components[id] = actions;
}
self.get = function(id) {
return components[id];
}
})
The components registry has methods for storing and getting components by ids. Of course, there might be much more methods and they might be more complicated, but this is just a simple example.
2. Service for our directive
Let's say we have a simple show-message directive, so we can create a $showMessageService:
angular.module('app')
.service('$showMessageService', ['$componentsRegistry', function($componentsRegistry) {
return function(id) {
return $componentsRegistry.get(id);
}
}])
For now, the only task of the service is to return our directive's actions. But it can be extended in the future, of course.
3. Directive's code
The last thing we need is our show-message directive:
angular.module('app')
.directive('showMessage', function($componentsRegistry) {
return {
restrict: 'E',
scope: {
directiveId: '#' // Unique id is passed from the view
},
template: '<div>{{ text }}</div>',
link: function(scope) {
scope.text = 'TEST TEXT';
// Create actions
scope.actions = {
set: function(value) {
scope.text = value;
}
}
// Store actions in the components registry
$componentsRegistry.put(scope.directiveId, scope.actions);
}
}
})
In a link function, we need to simply register our actions in components registry. We pass the unique id from the view so that developer has control over it inside views/controllers.
Example of usage
And now we can finally use the directive in our application. Here is a simple code which shows how to do that:
View
<div ng-controller="testController as test">
<show-message directive-id="testDirective"></show-message>
<button ng-click="test.changeText()">CHANGE</button>
</div>
Controller
angular.module('app')
.controller('testController', function(['$showMessageService', $showMessageService) {
var self = this;
self.changeText = function() {
$showMessageService('testDirective').set('NEW');
}
}])
I've Change The Directive Code, moved Controller code to link and it's working fine.
testApp.directive("helloDirective", function () {
return {
restrict: "E",
template: '<input type="text" data-ng-model="model.msg" />',
scope: {},
bindToController: {
param: "="
},
link: function (scope, element, attrs) {
var self = scope.model;
var assignMethod = function () {
if (self.param == undefined) {
self.param = {};
}
self.param.actions = {
get: function () {
return self.msg;
},
set: function (msgData) {
self.msg = msgData;
}
};
};
assignMethod();
},
controller: function () { },
controllerAs: 'model'
}
});
now i can call the directive get Method from calling controller like,
self.helloParam = {};
self.click = function () {
alert(self.helloParam.actions.get());
}
I am having a hard time trying to figure out how I mock out a required controller for a directive I have written that's the child of another.
First let me share the directives I have:
PARENT
angular
.module('app.components')
.directive('myTable', myTable);
function myTable() {
var myTable = {
restrict: 'E',
transclude: {
actions: 'actionsContainer',
table: 'tableContainer'
},
scope: {
selected: '='
},
templateUrl: 'app/components/table/myTable.html',
controller: controller,
controllerAs: 'vm',
bindToController: true
};
return myTable;
function controller($attrs, $scope, $element) {
var vm = this;
vm.enableMultiSelect = $attrs.multiple === '';
}
}
CHILD
angular
.module('app.components')
.directive('myTableRow', myTableRow);
myTableRow.$inject = ['$compile'];
function myTableRow($compile) {
var myTableRow = {
restrict: 'A',
require: ['myTableRow', '^^myTable'],
scope: {
model: '=myTableRow'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
link: link
};
return myTableRow;
function link(scope, element, attrs, ctrls) {
var self = ctrls.shift(),
tableCtrl = ctrls.shift();
if(tableCtrl.enableMultiSelect){
element.prepend(createCheckbox());
}
self.isSelected = function () {
if(!tableCtrl.enableMultiSelect) {
return false;
}
return tableCtrl.selected.indexOf(self.model) !== -1;
};
self.select = function () {
tableCtrl.selected.push(self.model);
};
self.deselect = function () {
tableCtrl.selected.splice(tableCtrl.selected.indexOf(self.model), 1);
};
self.toggle = function (event) {
if(event && event.stopPropagation) {
event.stopPropagation();
}
return self.isSelected() ? self.deselect() : self.select();
};
function createCheckbox() {
var checkbox = angular.element('<md-checkbox>').attr({
'aria-label': 'Select Row',
'ng-click': 'vm.toggle($event)',
'ng-checked': 'vm.isSelected()'
});
return angular.element('<td class="md-cell md-checkbox-cell">').append($compile(checkbox)(scope));
}
}
function controller() {
}
}
So as you can probably see, its a table row directive that prepends checkbox cells and when toggled are used for populating an array of selected items bound to the scope of the parent table directive.
When it comes to unit testing the table row directive I have come across solutions where can mock required controllers using the data property on the element.
I have attempted this and am now trying to test the toggle function in my table row directive to check it adds an item to the parent table directive's scope selected property:
describe('myTableRow Directive', function() {
var $compile,
scope,
compiledElement,
tableCtrl = {
enableMultiSelect: true,
selected: []
},
controller;
beforeEach(function() {
module('app.components');
inject(function(_$rootScope_, _$compile_) {
scope = _$rootScope_.$new();
$compile = _$compile_;
});
var element = angular.element('<table><tbody><tr my-table-row="data"><td></td></tr></tbody></table>');
element.data('$myTableController', tableCtrl);
scope.data = {foo: 'bar'};
compiledElement = $compile(element)(scope);
scope.$digest();
controller = compiledElement.controller('myTableRow');
});
describe('select', function(){
it('should work', function(){
controller.toggle();
expect(tableCtrl.selected.length).toEqual(1);
});
});
});
But I'm getting an error:
undefined is not an object (evaluating 'controller.toggle')
If I console log out the value of controller in my test it shows as undefined.
I am no doubt doing something wrong here in my approach, can someone please enlighten me?
Thanks
UPDATE
I have come across these posts already:
Unit testing a directive that defines a controller in AngularJS
How to access controllerAs namespace in unit test with compiled element?
I have tried the following, given I'm using controllerAs syntax:
var element = angular.element('<table><tr act-table-row="data"><td></td></tr></table>');
element.data('$actTableController', tableCtrl);
$scope.data = {foo: 'bar'};
$compile(element)($scope);
$scope.$digest();
console.log(element.controller('vm'));
But the controller is still coming up as undefined in the console log.
UPDATE 2
I have come across this post - isolateScope() returning undefined when testing angular directive
Thought it could help me, so I tried the following instead
console.log(compiledElement.children().scope().vm);
But still it returns as undefined. compiledElement.children().scope() does return a large object with lots of angular $$ prefixed scope related properties and I can see my vm controller I'm trying to get at is buried deep within, but not sure this is the right approach
UPDATE 3
I have come across this article which covers exactly the kind of thing I'm trying to achieve.
When I try to implement this approach in my test, I can get to the element of the child directive, but still I am unable to retrieve it's scope:
beforeEach(function(){
var element = angular.element('<table><tr act-table-row="data"><td></td></tr></table>');
element.data('$actTableController', tableCtrl);
$scope.data = {foo: 'bar'};
compiledElement = $compile(element)($scope);
$scope.$digest();
element = element.find('act-table-row');
console.log(element);
console.log(element.scope()); //returns undefined
});
I just wonder if this is down to me using both a link function and controllerAs syntax?
You were very close with the original code you'd posted. I think you were just using .controller('myTableRow') on the wrong element, as your compiledElement at this point was the whole table element. You needed to get a hold of the actual tr child element in order to get the myTableRow controller out of it.
See below, specifically:
controller = compiledElement.find('tr').controller('myTableRow');
/* Angular App */
(function() {
"use strict";
angular
.module('app.components', [])
.directive('myTableRow', myTableRow);
function myTableRow() {
return {
restrict: 'A',
require: ['myTableRow', '^^myTable'],
scope: {
model: '=myTableRow'
},
controller: controller,
controllerAs: 'vm',
bindToController: true,
link: link
};
function link($scope, $element, $attrs, $ctrls) {
var self = $ctrls.shift(),
tableCtrl = $ctrls.shift();
self.toggle = function() {
// keeping it simple for the unit test...
tableCtrl.selected[0] = self.model;
};
}
function controller() {}
}
})();
/* Unit Test */
(function() {
"use strict";
describe('myTableRow Directive', function() {
var $compile,
$scope,
compiledElement,
tableCtrl = {},
controller;
beforeEach(function() {
module('app.components');
inject(function(_$rootScope_, _$compile_) {
$scope = _$rootScope_.$new();
$compile = _$compile_;
});
tableCtrl.enableMultiSelect = true;
tableCtrl.selected = [];
var element = angular.element('<table><tbody><tr my-table-row="data"><td></td></tr></tbody></table>');
element.data('$myTableController', tableCtrl);
$scope.data = {
foo: 'bar'
};
compiledElement = $compile(element)($scope);
$scope.$digest();
controller = compiledElement.find('tr').controller('myTableRow');
//console.log(controller); // without the above .find('tr'), this is undefined
});
describe('select', function() {
it('should work', function() {
controller.toggle();
expect(tableCtrl.selected.length).toEqual(1);
});
});
});
})();
<link rel="stylesheet" href="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.css" />
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.js"></script>
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine-html.js"></script>
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/boot.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular-mocks.js"></script>
Here is an example to quote the use of angular directives using the parent child relationship.
The definition of annotated-image looks like this:(which is the parent)
angular.module('annotatedimage').directive('annotatedImage', function() {
function AnnotatedImageController(scope) {}
return {
{
restrict: 'E',
template: [
'<annotated-image-controls annotations="configuration.annotations"></annotated-image-controls>',
'<annotated-image-viewer src="configuration.image" annotations="configuration.annotations"></annotated-image-viewer>',
'<annotated-image-current></annotated-image-current>'
].join('\n'),
controller: ['$scope', AnnotatedImageController],
scope: {
configuration: '='
}
}
};
});
Now for the annotatedImageController , annotatedImageViewer and the annotatedImageCurrent which are the children.
angular.module('annotated-image').directive('annotatedImageControls', function() {
function link(scope, el, attrs, controller) {
scope.showAnnotations = function() {
controller.showAnnotations();
};
controller.onShowAnnotations(function() {
scope.viewing = true;
});
}
return {
restrict: 'E',
require: '^annotatedImage',
template: [
'<div>',
'<span span[data-role="show annotations"] ng-click="showAnnotations()" ng-hide="viewing">Show</span>',
'<span span[data-role="hide annotations"] ng-click="hideAnnotations()" ng-show="viewing">Hide</span>',
'<span ng-click="showAnnotations()">{{ annotations.length }} Annotations</span>',
'</div>'
].join('\n'),
link: link,
scope: {
annotations: '='
}
};
});
angular.module('annotated-image').directive('annotatedImageViewer', function() {
function link(scope, el, attrs, controller) {
var canvas = el.find('canvas');
var viewManager = new AnnotatedImage.ViewManager(canvas[0], scope.src);
controller.onShowAnnotations(function() {
viewManager.showAnnotations(scope.annotations);
});
}
return {
restrict: 'E',
require: '^annotatedImage',
template: '<canvas></canvas>',
link: link,
scope: {
src: '=',
annotations: '='
}
};
});
The same can be done for the annotatedImageCurrent
Summary
<parent-component>
<child-component></child-component>
<another-child-component></another-child-component>
</parent-component>
Parent Component
module.directive('parentComponent', function() {
function ParentComponentController(scope) {
// initialize scope
}
ParentComponentController.prototype.doSomething = function() {
// does nothing here
}
return {
restrict: 'E',
controller: ['$scope', ParentComponentController],
scope: {}
};
});
Child Component
module.directive('childComponent', function() {
function link(scope, element, attrs, controller) {
controller.doSomething();
}
return {
restrict: 'E',
require: '^parentComponent',
link: link,
scope: {}
}
});
I cannot get the parent function call from the isolated scope..The purpose of this code is to create a widget directive which can be used multiple times on the same page... I tried some other option, but doesn't work either. It works using the parent scope.
What am I missing here.
var app = angular.module("winApp", []);
app.controller("winCtrl", function($scope, dataFactory) {
$scope.getData = function() {
dataFactory.get('accounts.json').then(
function(data) {
$scope.items = data;
});
};
});
app.directive("windowSmall", function() {
return {
restrict : 'EA',
replace : 'true',
scope : {
type : '&'
},
transclude: 'true',
templateUrl : 'windowtemplate.html',
link : function(scope, element, attrs) {
element.bind("load", function(){
console.log(attrs.type);
if (angular.equals(attrs.type, 'getData()')) {
scope.active = 'accounts';
console.log(attrs.type);
// scope.getData();
scope.$apply(function() {
scope.$eval(attrs.type);
});
}
});
}
};
});
app.factory('dataFactory', function($http) {
return {
get : function(url) {
return $http.get(url).then(function(resp) {
return resp.data;
});
}
};
});
HTML:
<div ng-app="winApp" ng-controller="winCtrl">
<window-small type = "getData()"> </window-small>
<br> <br>
<!--
<window-small type = "bulletin"> </window-small> -->
You can also use $rootScope for a full proof solution. Due to the fact that an application can have multiple parents but only one $rootScope.
https://docs.angularjs.org/api/ng/service/$rootScope
Replace your link function with :
link : function(scope, element, attrs) {
element.bind("load", function(){
console.log(attrs.type);
if (angular.equals(attrs.type, 'getData()')) {
scope.active = 'accounts';
console.log(attrs.type);
scope.type();
}
});
}
Fiddle : http://jsfiddle.net/X7Fjm/3/
In documentation I can read next for the require option:
When a directive uses this option, $compile will throw an error
unless the specified controller is found. The ^ prefix means that this
directive searches for the controller on its parents (without the ^
prefix, the directive would look for the controller on just its own
element).
So I try to use it:
<div ng-sparkline></div>
app.directive('ngCity', function() {
return {
controller: function($scope) {}
}
});
app.directive('ngSparkline', function() {
return {
restrict: 'A',
require: '^ngCity',
scope: {},
template: '<div class="sparkline"><h4>Weather </h4></div>',
link: function(scope, iElement, iAttrs) {
// get weather details
}
}
});
But I have an error if my html have not ng-city attribute, so if I need controller of another directive - need to add exactly same attribute in html, but why (<div ng-sparkline ng-city="San Francisco"></div>)? And it looks on another directive's controller with this name (directive!!!) but not at controller with this name, is that true? Thanks. Just want to make it clear
With require you can get the controller of another (cooperating) directive. The controller in Angular is not semantically a function, but an object constructor, i.e. called essentially as var c = new Controller() (this is a simplification for the sake of clarity). Since the controller is an object, it can have properties and methods. By requiring the controller of another directive, you gain access to those properties/methods. Modifying your example to demonstrate:
app.directive('ngCity', function() {
return {
controller: function($scope) {
this.doSomething = function() {
...
};
}
}
});
app.directive('ngSparkline', function() {
return {
restrict: 'A',
require: '^ngCity',
scope: {},
template: '<div class="sparkline"><h4>Weather </h4></div>',
link: function(scope, iElement, iAttrs, ngCityController) {
// use the controller, e.g.
ngCityController.doSomething();
}
}
});
In your case, the city would be a property of the controller of the ngCity directive, exposed as a property. It will be read by the ngSparkline to know for which city the graph is about.
<b> added directives.js</b>
<code>
app.directive('ngSparkline', function () {
return {
restrict: 'A',
require: '^ngCity',
scope: {
ngCity: '#'
},
templateUrl: '/scripts/templates/tpl.html',
controller: ['$scope', '$http', function ($scope, $http) {
var url = "https://api.openweathermap.org/data/2.5/forecast/daily?mode=json&units=imperial&cnt=7&callback=JSON_CALLBACK&q=";
console.log(url + $scope.ngCity);
$scope.showTemp = function () {
$scope.getTemp($scope.ngCity);
};
$scope.getTemp = function (city) {
$http({
method: 'JSONP',
url: url + city
}).success(function(data) {
var weather = [];
angular.forEach(data.list, function(value){
weather.push(value);
});
$scope.weather = weather;
});
}
}],
link: function (scope, iElement, iAttrs, ctrl) {
scope.getTemp(iAttrs.ngCity);
scope.$watch('weather', function (newVal) {
if (newVal) {
var highs = [];
angular.forEach(scope.weather, function (value) {
highs.push(value.temp.max);
});
//chartGraph(iElement, highs, iAttrs);
}
});
}
}
}).directive('ngCity', function () {
return {
controller: function ($scope) {
//console.log("hello");
}
}
});
</code>
<b> and added tpl.htm</b>
<code>
<div class="sparkline">
<input type="text" data-ng-model="ngCity">
<button ng-click="showTemp()" class="btn1">Check {{ngCity}}</button>
<div style="color:#2743EF">{{weather}} ÂșC</div>
<div class="graph"></div>
</div>
</code>