I have created the following simple code with several directives (I have experience with Angular only for several months).
I have used unusual way to make some basic inheritance (as You can see from the Directive class), parameters by default (using ||), possibility to insert one element into another (using transclude), have added directory-specific controllers and of course, specified the link function to support DOM events handlers.
Currently I am trying to call function specified in another directory controller through it's scope, but it fails. The error is 'Uncaught TypeError: undefined is not a function'.
My target is to call controller's function independently from the DOM structure (the target element could be sibling, parent or child). I have considered also shared service and broadcast events, but it seems not the right way to make the communication.
Why the code doesn't work properly?
Are there any other ways to make it?
Thanks in advance.
HTML
<body ng-app="app">
<component id="button" name="button">
</component>
<icon name="icon">
</icon>
</body>
JS
(function() {
var app = angular.module('app', []);
app.directive('icon', function() {
var directive = new Directive();
return directive;
});
app.directive('button', function() {
var directive = new Directive();
return directive;
});
function Directive(options) {
options = options || {};
this.scope = options.scope || {
name: '#'
};
this.restrict = options.restrict || 'E';
this.replace = (options.replace != null) ? options.replace : true;
this.transclude = (options.transclude != null) ? options.transclude : true;
this.template = options.template || '<div ng-transclude></div>';
this.controller = function($scope) {
$scope.focus = function() {
console.log('focus');
};
$scope.outFocus = function() {
console.log('outFocus');
};
};
this.link = options.link || function($scope, $element, $attrs) {
$element.click(function() {
$scope.focus();
var _scope = angular.element('#button').scope();
_scope.outFocus(); // doesn't work
});
};
}
})();
Related
I'm trying to insert HTML into my div (bottom of code). I've dealt with an issue like this before so I added a filter. However, when the div is made visible through a toggle function the HTML doesn't display from the service. I have verified that the service is returning the proper HTML code.
The div is unhidden but no html is displayed.
Angular Code:
var myApp = angular.module('myApp', []);
angular.module('myApp').filter('unsafe', function ($sce) {
return function (val) {
if ((typeof val == 'string' || val instanceof String)) {
return $sce.trustAsHtml(val);
}
};
});
myApp.controller('myAppController', function ($scope, $http) {
...
SERVICE CODE
...
$scope.toggleHTMLResults();
$scope.HTMLjson = obj[0].HTML;
HTML Code:
<div id="returnedHTML" ng-bind-html="HTMLjson | unsafe " ng-hide="HTMLResults">NOT HIDDEN</div>
I'm not sure why this isn't working.
Here is my Plunker
There were multiple things wrong with your example.
Main Javascript file declared twice, first in header and second before close on body tag
You call a function as HTMLAPI() instead of $scope.HTMLAPI()
Your $scope.HTMLAPI() function was also being called before it was initialised
Fixed controller code:
app.controller('myAppCTRL', ['$scope', '$http', function ($scope, $http) {
var API = this;
$scope.HTMLInput = true;
$scope.HTMLResults = true;
$scope.toggleHTMLInput = function () {
$scope.HTMLInput = $scope.HTMLInput === false ? true : false;
}
$scope.toggleHTMLResults = function () {
$scope.HTMLResults = $scope.HTMLResults === false ? true : false;
}
$scope.HTMLAPI = function (HTML) {
var newJSON = ["[{\"ConditionId\":1111,\"ConditionDescription\":\"<i>DATA GOES HERE</i>\",\"ErrorId\":0,\"DisplayId\":0,\"DisplayName\":\"\",\"ErrorValue\":\"\"}]"];
var obj = JSON.parse(newJSON);
$scope.HTMLjson = obj[0].ConditionDescription;
$scope.toggleHTMLResults();
console.log($scope.HTMLjson);
}
$scope.HTMLAPI();
}]);
Working Example
I am building a sort of (faux) loader in Angular. Currently, I have this:
const app = angular.module('app', []);
app.controller('loaderCtrl', ($scope, $timeout) => {
let loading = $scope.loading,
loaded = $scope.loaded;
$scope.reset = () => {
$timeout(() => {
loading = false;
loaded = false;
console.log(loaded);
}, 500);
}
});
HTML:
<main ng-app="app">
<div ng-controller="loaderCtrl as loader" >
<div class="loader" ng-class="{ '-loading' : loader.loading === true, '-loaded' : loader.loaded === true }"></div>
<button ng-click="loader.loading = true;">loading</button>
<button ng-click="loader.loaded = true; reset();">loaded</button>
</div>
</main>
CodePen: http://codepen.io/tomekbuszewski/pen/WrXXdp
My problem is, both loading and loaded aren't being set up for my view, so the classes are permanently there. What can I do?
So, this is a problem of scope. Basically when you do this
let loading = $scope.loading,
loaded = $scope.loaded;
You get the "value" of the variables inside Angular scope. Therefore Angular does not know anything about changes made to those
The fix is simple, don't do that, but instead
$scope.reset = () => {
$timeout(() => {
$scope.loading = false;
$scope.loaded = false;
}, 500);
}
Why not using an object and change its content? It is possible to do that as #beaver pointed out, but then you have another problem, you need to trigger the digest cycle yourself via $apply. And somewhere in your code, you might accidentally change the content of the object and it might affect other part of the system
Having said that I do not know Babel and so I worked on the JS compiled version, I noticed that you assigned loader.loading and loader.loaded to variables and then used those "references" in $timeout function.
As in javascript
Primitives are passed by value, Objects are passed by "copy of a
reference"
you have to use $scope.loader.loading and $scope.loader.loaded
app.controller('loaderCtrl', function ($scope, $timeout) {
$scope.loader = {};
var loading = $scope.loader.loading, loaded = $scope.loader.loaded;
$scope.reset = function () {
$timeout(function () {
$scope.loader.loading = false;
$scope.loader.loaded = false;
}, 500);
};
});
Here I forked your CodePen: http://codepen.io/beaver71/pen/wMPprm
I want to load a state as a modal so that I can overlay a state without effecting any other states in my application. So for example if I have a link like:
<a ui-sref="notes.add" modal>Add Note</a>
I want to then interrupt the state change using a directive:
.directive('modal', ['$rootScope', '$state', '$http', '$compile',
function($rootScope, $state, $http, $compile){
return {
priority: 0,
restrict: 'A',
link: function(scope, el, attrs) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
event.preventDefault();
});
el.click(function(e){
$http
.get('URL HERE')
.then(function(resp){
$('<div class="modal">' + resp.data + '</div>').appendTo('[ui-view=app]');
setTimeout(function(){
$('.wrapper').addClass('showModal');
},1);
});
});
}
}
}
])
This successfully prevents the state change and loads the URL and appends it as a modal to the application. The problem is that it loads the entire application again...
How can I load just the state? e.g. the template files and the adjoining controller.
The state looks like:
.state('notes.add',
{
parent: 'notes',
url: '/add',
views: {
'content': {
templateUrl: 'partials/notes/add.html',
controller: 'NotesAddCtrl'
}
}
})
An example of how it should work using jQuery: http://dev.driz.co.uk/AngularModal
See how I can access StateA and StateB loading via AJAX that uses the History API to change the URL to reflect the current state change.
And regardless of whether I am on the index, StateA or StateB I can load StateA or StateB as a modal (even if I'm on that State already) and it doesn't change the url or the current content, it just overlays the state content.
This is what I want to be able to do in AngularJS.
Note. this example doesn't work with the browser back and forward buttons due to it being a quick example and not using the history api correctly.
I've seen your question a few days ago and it seemed interesting enough to try and set up something that would work.
I've taken the uiSref directive as a start, and modified the code to use angular-bootstrap's $modal to show the desired state.
angular.module('ui.router.modal', ['ui.router', 'ui.bootstrap'])
.directive('uiSrefModal', $StateRefModalDirective);
function parseStateRef(ref, current) {
var preparsed = ref.match(/^\s*({[^}]*})\s*$/), parsed;
if (preparsed) ref = current + '(' + preparsed[1] + ')';
parsed = ref.replace(/\n/g, " ").match(/^([^(]+?)\s*(\((.*)\))?$/);
if (!parsed || parsed.length !== 4) throw new Error("Invalid state ref '" + ref + "'");
return { state: parsed[1], paramExpr: parsed[3] || null };
}
function stateContext(el) {
var stateData = el.parent().inheritedData('$uiView');
if (stateData && stateData.state && stateData.state.name) {
return stateData.state;
}
}
$StateRefModalDirective.$inject = ['$state', '$timeout', '$modal'];
function $StateRefModalDirective($state, $timeout, $modal) {
var allowedOptions = ['location', 'inherit', 'reload'];
return {
restrict: 'A',
link: function(scope, element, attrs) {
var ref = parseStateRef(attrs.uiSrefModal, $state.current.name);
var params = null, url = null, base = stateContext(element) || $state.$current;
var newHref = null, isAnchor = element.prop("tagName") === "A";
var isForm = element[0].nodeName === "FORM";
var attr = isForm ? "action" : "href", nav = true;
var options = { relative: base, inherit: true };
var optionsOverride = scope.$eval(attrs.uiSrefModalOpts) || {};
angular.forEach(allowedOptions, function(option) {
if (option in optionsOverride) {
options[option] = optionsOverride[option];
}
});
var update = function(newVal) {
if (newVal) params = angular.copy(newVal);
if (!nav) return;
newHref = $state.href(ref.state, params, options);
if (newHref === null) {
nav = false;
return false;
}
attrs.$set(attr, newHref);
};
if (ref.paramExpr) {
scope.$watch(ref.paramExpr, function(newVal, oldVal) {
if (newVal !== params) update(newVal);
}, true);
params = angular.copy(scope.$eval(ref.paramExpr));
}
update();
if (isForm) return;
element.bind("click", function(e) {
var button = e.which || e.button;
if ( !(button > 1 || e.ctrlKey || e.metaKey || e.shiftKey || element.attr('target')) ) {
e.preventDefault();
var state = $state.get(ref.state);
var modalInstance = $modal.open({
template: '<div>\
<div class="modal-header">\
<h3 class="modal-title">' + ref.state + '</h3>\
</div>\
<div class="modal-body">\
<ng-include src="\'' + state.templateUrl + '\'"></ng-include>\
</div>\
</div>',
controller: state.controller,
resolve: options.resolve
});
modalInstance.result.then(function (selectedItem) {
$scope.selected = selectedItem;
}, function () {
console.log('Modal dismissed at: ' + new Date());
});
}
});
}
};
}
You can use it like this <a ui-sref-modal="notes.add">Add Note</a>
Directive requires the angular-bootstrap to resolve the modal dialog. You will need to require the ui.router.modal module in your app.
Since asked to provide an example for my comment,
Example Directive
myapp.directive('openModal', function ($modal) {
return function(scope, element, attrs) {
element[0].addEventListener('click', function() {
$modal.open({
templateUrl : attrs.openModal,
controller: attrs.controller,
size: attrs.openModalSize,
//scope: angular.element(element[0]).scope()
});
});
};
});
Example Html
<button
open-modal='views/poc/open-modal/small-modal.html'
open-modal-size='sm'
controller="MyCtrl">modal small</button>
The above directive approach is not very different from using a state, which has templateUrl and controller except that url does not change.
.state('state1.list', {
url: "/list",
templateUrl: "partials/state1.list.html",
controller: function($scope) {
$scope.items = ["A", "List", "Of", "Items"];
}
})
Apparently there is the issue Ui-sref not generating hash in URL (Angular 1.3.0-rc.3) refering to
https://github.com/angular-ui/ui-router/issues/1397
It is seems to be fixed as per comments.
I personally dislike html5mode because it requires extra work on your server for no apparent advantage (I don't regard having "more beautiful url" as tangible advantage to justify the extra work).
There is another performance problem when using routers, that the view DOM is re-created upon each route change. I mentioned a very simple solution
in this answer.
As a side remark, the example in http://dev.driz.co.uk/AngularModal/ does not behave quite well. It does not record history, so I can't go back. Further, if you click on links like Index or modals, and then reload, you don't get the same page.
UPDATE.
It seems from the comments that a route change is not wanted when opening the modal. In that case the easiest solution is not to put ui-sref on the opening button and let the modal directive along handle it.
I have an interesting situation.
I have a directive with isolate scope that generate list of numbers and the user can choose numbers like in lottery.
The problem i have is that i required minimum of 1 line, if the user pick only one line so when he click play i want to auto trigger the next directive in the ng-repeat to pick for him numbers, I made this plunker so you guys can understand better and help me.
http://plnkr.co/edit/vWGmSEpinf7wxRUnqyWq?p=preview
<div ng-repeat="line in [0,1,2,3]">
<div line line-config="lineConfig">
</div>
</div>
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.lineConfig = {
guessRange: 10
}
$scope.lines = [];
$scope.$on('lineAdded', function(event, line) {
$scope.lines.push(line);
});
$scope.play = function() {
/// here i want to check if $scope.lines.length
//is less then one if yes then auto trigger the next
//line directive to auto do quick pick and continue
}
})
.directive('line', function() {
return {
restrict: 'A',
templateUrl: 'line.html',
scope: {
lineConfig: '='
},
link: function($scope, elem, attr) {
var guessRange = $scope.lineConfig.guessRange;
$scope.cells = [];
$scope.line = {
nums: []
};
$scope.$watch('line', function(lotLine) {
var finaLine = {
line: $scope.line
}
if ($scope.line.nums.length > 4) {
$scope.$emit('lineAdded', finaLine);
}
}, true);
_(_.range(1, guessRange + 1)).forEach(function(num) {
$scope.cells.push({
num: num,
isSelected: false
});
});
$scope.userPickNum = function(cell) {
if (cell.isSelected) {
cell.isSelected = false;
_.pull($scope.lotLine.nums, cell.num);
} else {
cell.isSelected = true;
$scope.lotLine.nums.push(cell.num);
}
};
$scope.quickPick = function() {
$scope.clearLot();
$scope.line.nums = _.sample(_.range(1, guessRange + 1), 5);
_($scope.line.nums).forEach(function(num) {
num = _.find($scope.cells, {
num: num
});
num.isSelected = true;
});
}
$scope.clearLot = function() {
_($scope.cells).forEach(function(num) {
num.isSelected = false;
});
$scope.line.nums = [];
}
}
}
})
You could pass the $index (exists automatically in the ng-repeat scope) - variable into the directive and cause it to broadcast an event unique for ($index + 1) which is the $index for the next instance.
The event could be broadcasted from the $rootScope or a closer scope that's above the repeat.
Then you could capture the event in there.
Probably not the best way to do it.
I can try to elaborate if anything is unclear.
EDIT
So I played around alittle and came up with this:
http://plnkr.co/edit/ChRCyF7yQcN580umVfX1?p=preview
Rather
Rather than using events or services I went with using a directive controller to act as the parent over all the line directives inside it:
.directive('lineHandler', function () {
return {
controller: function () {
this.lines = [];
}
}
})
Then requiring 'lineHandler' controller inside the 'line' directive - the controller being a singleton (same instance injected into all the line directives) - you can then setup that controller to handle communication between your directives.
I commented most of my code in the updated plnkr and setup an example of what I think you requested when clicking in one list - affecting the one beneath.
I hope this helps and if anything is unclear I will try to elaborate.
I just found out how to communicate between controllers using $broadcast and $emit, tried it in my POC and it worked, sort of, the original problem described in this other post is still not solved but now I have another question, the event is being registered multiple times so I am trying to unregister it the way I've seen it in multiple posts here on SO but now the event won't fire. The code is as follows:
tabsApp.controller('BasicOverviewController', function ($scope, $location, $rootScope) {
var unbind = $rootScope.$on('displayModal', function (event, data) {
if (data.displayModal) {
alert("I want to display a modal!");
var modal = $('#basicModal');
modal.modal('toggle');
}
});
$scope.$on('$destroy', function () {
unbind();
});
});
tabsApp.controller('SportsController', function SportsController($scope, $location, $rootScope) {
$scope.goToOverview = function (showModal) {
$location.path("overview/basic");
$rootScope.$emit('displayModal', { displayModal: showModal })
};
});
If I remove the
var unbind = ...
the event fires and I can see the alert. As soon as I add the code to unregister the event, the code is never fired. How can the two things work together?
Could you just pull out unbind into its own function, and use it in both like this?
tabsApp.controller('BasicOverviewController', function ($scope, $location, $rootScope) {
var unbind = function (event, data) {
if (data.displayModal) {
alert("I want to display a modal!");
var modal = $('#basicModal');
modal.modal('toggle');
}
};
$rootScope.$on('displayModal', unbind);
$scope.$on('$destroy', unbind);
});
I could be wrong but my guess would be that the BasicOverviewController isn't being persisted and it's scope is being destroyed before the SportsController gets a chance to utilize it. Without a working example, I can't deduce much more. If you want to maintain this on $rootScope then a possible pattern would be:
if (!$rootScope.displayModalDereg) {
$rootScope.displayModalDereg = $rootScope.$on('displayModal', function (event, data) {
if (data.displayModal) {
alert("I want to display a modal!");
var modal = $('#basicModal');
modal.modal('toggle');
}
});
This also allows you to check and see if there is an event registered so you can dereg it if needed.
if ($rootScope.displayModalDereg) {// this event has been registered
$rootScope.displayModalDereg();
$rootScope.dispalyModalDereg = undefined;
}
I would heavily suggested creating a displayModal directive that persists all of this instead of maintaining it on $rootScope. Obviously you would still $emit, or better yet, $broadcast from $rootScope, just not persist the dereg function there.
Here is an example of a modal directive I once wrote:
/**
*
* Modal Directive
*/
'use strict';
(function initModalDrtv(window) {
var angular = window.angular,
app = window.app;
angular.module(app.directives).directive('modalDrtv', [
'$rootScope',
function modalDrtv($rootScope) {
return {
restrict: 'A',
scope: {},
templateUrl: '/templates/modal.html',
replace: true,
compile: function modalCompileFn(tElement, tAttrs) {
return function modalLinkFn(scope, elem, attrs) {
scope.show = false;
scope.options = {
'title': '',
'message': '',
'markup': undefined,
'buttons': {
showCancel: false,
showSecondary: false,
secondaryAction: '',
primaryAction: 'Ok'
},
'responseName': ''
};
scope.respond = function(response) {
var r = '';
if (response === 1) {
r = scope.options.buttons.primaryAction;
} else if (response === 2) {
r = scope.options.buttons.secondaryAction;
} else {
r = response;
}
$rootScope.$broadcast(scope.options.responseName, r);
scope.show = false;
};
scope.$on('initIrpModal', function(event, data) {
if (angular.isUndefined(data)) throw new Error("Data missing from irp modal event");
scope.options.title = data.title;
scope.options.message = data.message;
scope.options.buttons.showCancel = data.buttons.showCancel;
scope.options.buttons.showSecondary = data.buttons.showSecondary;
scope.options.buttons.secondaryAction = data.buttons.secondaryAction;
scope.options.buttons.primaryAction = data.buttons.primaryAction;
scope.options.responseName = data.responseName;
scope.show = true;
});
}
}
}
}
]);
})(window);
This directive utilizes one modal and let's anything anywhere in the app utilize it. The registered event lives on its isolate scope and therefore is destroyed when the modal's scope is destroyed. It also is configured with a response name so that if a user response is needed it can broadcast an event, letting the portion of the app that initialized the modal hear the response.