I have the following markup:
<div class="row" loader="loader" address-book>
<!-- Loading complete - this should show result -->
<div loader-success></div>
<!-- Loading failed - this should show some error -->
<div loader-failure>Failure</div>
</div>
Here, address-book is the main app, and the loader is to only act as showing an animation while the page is loading. The content of the address-book would go into loader-success and the failure message (that is, if no content found) would go inside loader-failure.
So my idea was to use the following directives:
app.directive('loader', function($compile, $timeout)
{
return {
restrict: 'AE',
scope: {
loader: '&'
},
transclude: true,
template:
'<div class="row" ng-if="loading">' +
' <div class="small-15 columns text-center">' +
' <i class="fa fa-spin fa-circle-o-notch"></i>' +
' </div>' +
'</div>' +
'<div ng-transclude></div>',
link: function(scope, element, attrs)
{
scope.loading = true;
scope.failure = false;
scope.success = false;
scope.$watch('loader', function(fn) {
if (_.isFunction(fn)) {
scope.loader()
.then(function(){
scope.success = true;
scope.loading = false;
scope.$safeApply();
})
.catch(function() {
scope.failure = true;
scope.loading = false;
scope.$safeApply();
});
}
});
}
};
});
where scope.loader is the promise of the $q set from address-book. The promise is resolved/rejected after the address-book tries to get content.
For the success/failure blocks I have:
app.directive('loaderFailure', function()
{
return {
restrict: 'AE',
link: function(scope, element, attrs)
{
element.hide();
scope.$watch('failure', function(value) {
if (_.isTrue(value)) {
element.show();
}
});
}
};
});
app.directive('loaderSuccess', function()
{
return {
restrict: 'AE',
link: function(scope, element, attrs)
{
element.hide();
scope.$watch('success', function(value) {
if (_.isTrue(value)) {
element.show();
}
});
}
};
});
However, because of the isolate scope on loader, the scopes of the two children loader-success and loader-failure can no longer read the parent's. I cannot use the $compile on loader because that would then take the scopes out of the address-book's app.
What can I do?
You can use events. If you want to send some date from children to parent use $scope.$emit.
From parent to children $scope.$broadcast - can have negative performance impact
It it good approach to use events when you want to communicate between components that are loosely coupled.
See more info about events:
https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$emit
So I ended up using $emit. However, since my loader directive can be used on multiple different components on the same page, it was important to be able to distinguish between the two. Also, the loader is added to an element with another directive running on it.
To fix these issues, this is what I did:
Failure
The channel is then used as a unique identifier. It can even be the $id of the other directive scope to ensure that it is completely unique. Now, the directives are as follows:
app.directive('loader', function($compile, $rootScope)
{
return {
restrict: 'AE',
scope: {
loader: '&',
channel : '#loaderChannel'
},
transclude: true,
template:
'<div class="row" ng-if="loading">' +
' <div class="small-15 columns text-center loader">' +
' <i class="fa fa-spin fa-circle-o-notch"></i>' +
' </div>' +
'</div>' +
'<div ng-transclude></div>',
link: function(scope, element, attrs)
{
scope.loading = true;
scope.$watch('loader', function(fn) {
if (_.isFunction(fn)) {
scope.loader()
.then(function(){
scope.loading = false;
$rootScope.$emit('LOADER_SUCCESS', scope.channel);
scope.$safeApply();
})
.catch(function() {
scope.loading = false;
$rootScope.$emit('LOADER_FAILURE', scope.channel);
scope.$safeApply();
});
}
});
}
};
});
app.directive('loaderFailure', function($rootScope)
{
return {
restrict: 'AE',
link: function(scope, element, attrs)
{
element.hide();
$rootScope.$on('LOADER_FAILURE', function(event, channel) {
if (channel == attrs.loaderFailure) element.show();
});
}
};
});
app.directive('loaderSuccess', function($rootScope)
{
return {
restrict: 'AE',
link: function(scope, element, attrs)
{
element.hide();
$rootScope.$on('LOADER_SUCCESS', function(event, channel) {
if (channel == attrs.loaderSuccess) element.show();
});
}
};
});
Basically, I $emit the success or failure with the channel. And the loader-success and loader-failure listen to these broadcasts, match their channel it and behave accordingly.
In each component then, I simply define scope.loader = $q.defer() and resolve/reject it when I need.
Related
controller:
service.checkSub(function(data){
$scope.showSub = data.subscribe? false : true;
})
directive:
app.directive('showSub', function() {
return {
restrict: 'E',
replace: true,
scope: {
showSub: '=show'
},
templateUrl: '<div data-ng-show="show">test</div>',
link: function(scope, element, attrs) {
console.log(scope.showSub); // undifined
if(scope.showSub) {
scope.show = true;
}else {
scope.show = false;
}
}
}
});
<show-sub show="showSub"></show-sub>
why the scope.showSub in directive is undefined ,and I want to use it to control the directive? how should I do it?
The scope.showSub gives undefined, because when loading in the directive, the showSub of your controller scope isn't filled yet. What you can do to fix it:
Change templateUrl to template
Change ng-show="show" to ng-show="showSub"
Lose the link function (it is not needed, as you can directly bind to the scope variables in your template)
code:
app.directive('showSub', function($timeout) {
return {
restrict: 'E',
replace: true,
scope: {
showSub: '=show'
},
template: '<div data-ng-show="showSub">test</div>',
link: function(scope, elem) {
// this function isn't needed, but to show you it gives undefined due to the async call
console.log(scope.showSub); // undefined
$timeout(function(){
console.log(scope.showSub); // true
}, 1500);
}
}
});
Here is a jsfiddle
Your directive is fine but problem with the service.
service.checkSub(function(data){
$scope.showSub = data.subscribe? false : true;
})
$scope.showSub should be in parent scope.
make sure you have data in $scope.showSub
You can get value of showSub by scope.$parent.showSub
So your code will be like ..
app.directive('showSub', function() {
return {
restrict: 'E',
replace: true,
scope: {
showSub: '=show'
},
templateUrl: '<div data-ng-show="show">test</div>',
link: function(scope, element, attrs) {
console.log(scope.$parent.showSub);
if(scope.$parent.showSub) {
scope.show = true;
}else {
scope.show = false;
}
}
}
});
I have 2 directives: calculatorForm and calculatorAttribute. CalculatorForm is the parent directive, specifically a form which contains input tags which are calculatorAttribute directives.
I want the calculatorAttribute call calculatorForm function that changes a scope variable and trigger a watcher.
Here's my code:
angular
.module('calculator')
.directive('calculatorForm', ['CalculatorDataModel', 'CalculatorPriceModel',
function(CalculatorDataModel, CalculatorPriceModel) {
return {
restrict : 'A',
replace : true,
templateUrl : function(element, attrs) {
return attrs.templateUrl;
},
link : function(scope, element, attrs) {
scope.model = CalculatorDataModel;
scope.price = CalculatorPriceModel;
scope.model.initialize(calculator_data);
scope.updateSelectedSpecs = function(attribute_id, prod_attr_val_id) {
var selected_specs = JSON.parse(JSON.stringify(scope.model.selected_specs));
selected_specs[attribute_id] = prod_attr_val_id;
scope.model.selected_specs = selected_specs;
}
scope.$watch('model.selected_specs', function(selected_specs, previous_selected_specs) {
if (selected_specs != previous_selected_specs) {
scope.model.setCalculatorData();
scope.price.computePrice();
}
});
}
}
}
])
.directive('calculatorAttribute', [
function() {
return {
restrict : 'A',
template : "<input type='radio' name='attr{{attribute_id}}' ng-value='prod_attr_val_id'/>",
replace : true,
link : function(scope, element, attrs) {
scope.attribute_id = attrs.attributeId;
scope.prod_attr_val_id = attrs.prodAttrValId;
element.on('click', function() {
scope.$parent.updateSelectedSpecs(scope.attribute_id, scope.prod_attr_val_id);
});
}
}
}
]);
My problem is updateSelectedSpecs in the parent is called but watcher has never been triggered when I use element.on click in the child directive.
Please help everyone Thank you!!!
Okay, after wrestling with this for a bit, I managed to produce a working version of a slimmed-down example:
angular.module('myApp', [])
.directive('calculatorForm', function() {
return {
restrict: 'A',
replace: true,
transclude: true,
template: '<div ng-transclude></div>',
link: function(scope, element, attrs) {
scope.model = {};
scope.price = {};
scope.updateSelectedSpecs = function(attribute_id, prod_attr_val_id) {
scope.$apply(function() {
console.log('update selected specs');
var selected_specs = {};
selected_specs[attribute_id] = prod_attr_val_id;
scope.model.selected_specs = selected_specs;
});
}
scope.$watch('model.selected_specs', function(selected_specs, previous_selected_specs) {
console.log('new selected specs', selected_specs, previous_selected_specs);
if (selected_specs != previous_selected_specs) {
console.log("and they're different");
}
});
}
};
})
.directive('calculatorAttribute', function() {
return {
restrict: 'A',
template: "<input type='radio' name='attr{{attribute_id}}' ng-value='prod_attr_val_id'/>",
replace: true,
link: function(scope, element, attrs) {
scope.attribute_id = attrs.attributeId;
scope.prod_attr_val_id = attrs.prodAttrValId;
element.on('click', function() {
scope.$parent.updateSelectedSpecs(scope.attribute_id, scope.prod_attr_val_id);
});
}
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<form calculator-form ng-app="myApp">
<input calculator-attribute attribute-id=1 prod-attr-val-id=1>
</form>
Just look at the console to see it getting into the $watch. The problem seemed to be the fact that you didn't trigger a $digest cycle in your updateSelectedSpecs function. Usually a $timeout, $http call, or ngClick or other event would start the $digest cycle for you, but in this case you have to start it yourself using scope.$apply().
I have a custom directive with children directives:
<rp-nav>
<rp-nav-item cat="1"></rp-nav-item>
<rp-nav-item cat="2"></rp-nav-item>
<rp-nav-item cat="3"></rp-nav-item>
<rp-nav-item cat="4"></rp-nav-item>
<rp-flyout></rp-flyout>
</rp-nav>
Here are the modules I have defined:
var app = angular.module('app', []);
app.directive('rpNav', function() {
return {
restrict: 'E',
controller: function($scope) {
$scope.currentItem = 'none'; //initialize currentItem
this.setCurrentItem = function(itemId) {
$scope.currentItem = itemId;
}
},
};
});
app.directive('rpNavItem', function() {
return {
restrict: 'E',
template: function(el, attrs) {
return '<p>item {{currentItem}} ' + attrs.cat;
},
require: '^rpNav',
link: function(scope, el, attrs, nav) {
el.on('click', function() {
nav.setCurrentItem(attrs.cat);
});
}
};
});
app.directive('rpFlyout', function() {
return {
restrict: 'E',
template: '<p style="background-color: lightblue">{{currentItem}}</p>'
};
});
The idea is to click in any of the items and make the rp-flyout element display information about the clicked rp-nav-item. The scope variable currentItem does change on click, but the template in rp-flyout does not update. What am I missing to achieve this goal? And, is this a "best practice" way of tackling this problem.
Here's a plunker
To expand on the comment, directives are not inherently part of the digest cycle, so you need to add scope.$apply() inside your el.click handler to trigger a digest cycle and update template bindings.
el.on('click', function() {
nav.setCurrentItem(attrs.cat);
scope.$apply();
});
create the custom directive for radio buttons in angular js
i tried so far
HTML:
<my-raido></my-raido>
<my-raido></my-raido>
<my-raido></my-raido>
Angular js:
var App = angular.module('myRaido',[]);
App.directive('myRaido',function() {
return {
restrict : 'E',
template: '<div class="myClass"><input type="radio"></div>',
replace:true,
link:function (scope, element, attrs) {
element.click(function () {
console.log("clicked....");
// body...
});
console.log("link should be work as per our expections...", + attrs.valve);
}
}
});
Advance Thanks
You should set a name attribute on your radio elements to make them a part of a group.
angular.module('app', [])
.directive('myRadio', function() {
return {
restrict : 'E',
template: '<div class="myClass">\
<label>\
<input type="radio">\
<span></span>\
</label>\
</div>',
replace: true,
link: function (scope, element, attrs) {
var $radio = element.find('input');
var $span = element.find('span');
$radio.attr('name', attrs.group);
$span.html(attrs.label);
}
}
});
If more than one input[radio] controls have a same name, they will behave as a group.
You would use this directive like this:
<my-radio group="group1" label="Option1"></my-radio>
<my-radio group="group1" label="Option2"></my-radio>
<my-radio group="group1" label="Option2"></my-radio>
var App = angular.module('myRaido',[]);
App.directive('myRaido',function() {
return {
restrict : 'E',
template: '<div class="myClass"><input type="radio" name='fooRadios'></div>',
replace:true,
link:function (scope, element, attrs) {
element.click(function () {
console.log("clicked....");
// body...
});
console.log("link should be work as per our expections...", + attrs.valve);
}
}
});
This should not be too hard a thing to do but I cannot figure out how best to do it.
I have a parent directive, like so:
directive('editableFieldset', function () {
return {
restrict: 'E',
scope: {
model: '='
},
replace: true,
transclude: true,
template: '
<div class="editable-fieldset" ng-click="edit()">
<div ng-transclude></div>
...
</div>',
controller: ['$scope', function ($scope) {
$scope.edit = ->
$scope.editing = true
// ...
]
};
});
And a child directive:
.directive('editableString', function () {
return {
restrict: 'E',
replace: true,
template: function (element, attrs) {
'<div>
<label>' + attrs.label + '</label>
<p>{{ model.' + attrs.field + ' }}</p>
...
</div>'
},
require: '^editableFieldset'
};
});
How can I easily access the model and editing properties of the parent directive from the child directive? In my link function I have access to the parent scope - should I use $watch to watch these properties?
Put together, what I'd like to have is:
<editable-fieldset model="myModel">
<editable-string label="Some Property" field="property"></editable-string>
<editable-string label="Some Property" field="property"></editable-string>
</editable-fieldset>
The idea is to have a set of fields displayed by default. If clicked on, they become inputs and can be edited.
Taking inspiration from this SO post, I've got a working solution here in this plunker.
I had to change quite a bit. I opted to have an isolated scope on the editableString as well because it was easier to bind in the correct values to the template. Otherwise, you are going to have to use compile or another method (like $transclude service).
Here is the result:
JS:
var myApp = angular.module('myApp', []);
myApp.controller('Ctrl', function($scope) {
$scope.myModel = { property1: 'hello1', property2: 'hello2' }
});
myApp.directive('editableFieldset', function () {
return {
restrict: 'E',
scope: {
model: '='
},
transclude: true,
replace: true,
template: '<div class="editable-fieldset" ng-click="edit()"><div ng-transclude></div></div>',
link: function(scope, element) {
scope.edit = function() {
scope.editing = true;
}
},
controller: ['$scope', function($scope) {
this.getModel = function() {
return $scope.model;
}
}]
};
});
myApp.directive('editableString', function () {
return {
restrict: 'E',
replace: true,
scope: {
label: '#',
field: '#'
},
template: '<div><label>{{ label }}</label><p>{{ model[field] }}</p></div>',
require: '^editableFieldset',
link: function(scope, element, attrs, ctrl) {
scope.model = ctrl.getModel();
}
};
});
HTML:
<body ng-controller="Ctrl">
<h1>Hello Plunker!</h1>
<editable-fieldset model="myModel">
<editable-string label="Some Property1:" field="property1"></editable-string>
<editable-string label="Some Property2:" field="property2"></editable-string>
</editable-fieldset>
</body>
You can get access to parent controller by passing attribute in child directive link function
link: function (scope, element, attrs, parentCtrl) {
parentCtrl.$scope.editing = true;
}