Add dynamic angular attributes to dom element - angularjs

I have an ApplicationController that runs over my dynamic controller
This Application controller has an array that are my App-bar actions.
Well, every dynamic controller i have inherit the ability to change this array, once its on $scope.headers.actions;
What I'm trying to do right now is to create a directive where i'll be able to add angular attributes (ng-class, ng-if, etc...) dynamically where this attributes definition will come from $scope.headers.actions[...].attributes
Now my directive is:
app.directive('ngDynamicAttrs', ['$compile',function ($compile) {
return {
scope: { list: '=ngDynamicAttrs' },
//priority: 1001,
//terminal: true,
link: function (scope, elem, attrs) {
for (attr in scope.list) {
attrs.$set(attr, scope.list[attr]);
//elem[0].setAttribute(attr, scope.list[attr]);
//elem.attr(attr, scope.list[attr]);
}
$compile(elem)(scope);
}
};
}]);
My html is:
<ul class="actions">
<li class="action" ng-repeat="action in header.actions.pool" ng-click="action.events.click()" ng-dynamic-attrs="action.attributes" id="{{action.id}}">
<i ng-if="action.icon" class="icon fa {{action.icon}}"></i>
{{action.name}}
</li>
</ul>
And my action object is :
$scope.header = {
actions: {
pool: [],
clear: function (actions) {
$scope.header.actions.pool = [];
},
set: function (actions) {
$scope.header.actions.clear();
for (i in actions) {
$scope.header.actions.add(actions[i].id, actions[i].name, actions[i].icon, actions[i].events, actions[i].items, actions[i].attributes)
}
},
add: function (id, name, icon, events, items, attributes) {
$scope.header.actions.pool.push($scope.header.actions.create(id, name, icon, events, items, attributes));
},
remove: function (id) {
for (i in $scope.header.actions.pool) {
if ($scope.header.actions.pool[i].id == id) delete $scope.header.actions.pool[i];
}
},
create: function (id, name, icon, events, items, attributes) {
return { id: id, name: name, icon: icon, events: events, items: items, attributes: attributes };
}
}
}
Note: I'm not even considering the events right here because it's not a big deal once i can easily use normal dom events.

Related

How to pass methods with pre defined argument to Angular JS Directives

I am trying to add an Action Buttons to my DataTable depending if the status is true by passing a method to the directive.
CONTROLLER.JS
_ctr.route = 'home.stocks';
_ctr.object = [
{
id: 1,
name: 'XXXX',
status: true
}, {
id: 2,
name: 'XXXX',
status: false
}
];
_ctr.headers = {
name: 'NAME',
status: 'STATUS'
}
_ctr.method = row => {
if (row.type) {
return row['button'] = [
{
label: 'Edit',
class: 'primary',
state: _ctr.route + `.edit({ id : '${row.id}' })`
}, {
label: 'Delete',
class: 'danger',
state: _ctr.route + `.delete({ id : '${row.id}' })`
}
];
}
}
This is how I transfer variables to the Directive.
FORM.HTML
<template-table
tb-object="{{ _ctr.object }}"
tb-headers="{{ _ctr.headers }}"
tb-method="{{ _ctr.method() }}">
</template-table>
This is my customized Table.
TEMPLATE.JS
app.directive('templateTable', function (factory, $compile) {
return {
restrict: 'E',
replace: true,
scope: {
tbMethod: '#',
tbObject: '#',
tbHeaders: '#',
},
link: (scope, element, attrs) => {
attrs.$observe('tbObject', data => {
if (data) {
let object = scope.$eval(data);
let headers = scope.$eval(scope.tbHeaders);
let method = scope.tbMethod;
let headers_config = [];
// converting `headers` to DataTable Column Format
for (let prop in headers) {
headers_conf.push({
data: prop,
bSortable: true,
label: headers[prop]
});
}
if (method) {
headers_conf.push({
mData: null,
bSortable: false,
label: 'ACTION',
mRender: function (row, type, full) {
// calls method and passes `row` argument to method
// checks if `row.type` is `true`
row[method(row)];
let button = row.button;
let template = '';
// if true appends 'Action Button'
if (button) {
button.map(res => {
template +=
`<a ui-sref="${res.state}" class="btn btn-${res.class}
mr-2" style="color: white;" role="button"> ${res.label}
</a>`;
})
}
return `<td class="action-buttons"> ${template} </td>`;
}
});
}
scope.headers = headers_conf;
$("#listTable").DataTable({
data: object,
columns: headers_conf,
"fnDrawCallback": function (oSettings) {
$compile(angular.element($("#listTable")).contents())
(scope);
}
});
}
});
}
}
});
This doesn't seem to work for me. What should I do to make the code above, work.
To pass a method to directive with isolate scope, don't use curly brackets:
<template-table
tb-method="_ctr.method">
</template-table>
In the isolate scope directive, use one-way binding:
app.directive('templateTable', function (factory, $compile) {
return {
restrict: 'E',
scope: {
tbMethod: '<',
},
link: (scope, element, attrs) => {
if (scope.tbMethod) {
var row = "something";
//Invoke method
scope.tbMethod(row);
};
}
}
})
From the Docs:
scope
The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the directive's element. These local properties are useful for aliasing values for templates. The keys in the object hash map to the name of the property on the isolate scope; the values define how the property is bound to the parent scope, via matching attributes on the directive's element:
For more information, see
< or <attr - set up a one-way (one-directional) binding between a local scope property and an expression passed via the attribute attr. The expression is evaluated in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name.
AngularJS Comprehensive API Reference - scope

ng-click does not work in directive of inline magnific popup

I have created magnific popup directive in angularjs. It works fine ng-click event on description link when first time loads but after close popup and again click on description link it does not work.
Please refer below code:
PhotoCtrl.js
(function () {
'use strict';
function PhotoCtrl($rootScope, $scope, $state, HzServices, HzPhotoService) {
$scope.doAlbumList = function () {
var data = {q: $state.params.pid};
$scope.albums = {};
$scope.albumsCount = 0;
$scope.photos = {};
$scope.photosCount = 0;
$rootScope.arrData = [];
var deferred = HzServices.deferred("/api/album/list", 'GET', data);
deferred.then(
function (res) {
/*
* success in repsonse
* Share common photo & album data across all controllers, directives by services.
*/
var data = {album: {count: res.data.count.album, data: res.data.album}, photo: {count: res.data.count.photo, data: res.data.photo}};
/*
* Create an array of magnific inline popup content
*/
angular.forEach(data.photo.data, function (value, key) {
if (value.data.length > 0) {
angular.forEach(value.data, function (value_, key_) {
$rootScope.arrData.push({
imageDescription: value_.photo_description,
imageScale_img: "/resize/" + value_.module + "/" + value_.photo_name,
imageOriginal_href: "/" + value_.module + "/" + value_.photo_name
});
});
}
});
HzPhotoService.setSharedData(data);
$scope.albums = $rootScope.sharedData.album.data;
$scope.albumsCount = $rootScope.sharedData.album.count;
$scope.photos = $rootScope.sharedData.photo.data;
$scope.photosCount = $rootScope.sharedData.photo.count;
},
function (res) {
/*
* Error hading in repsonse
*/
var data = {album: {count: $scope.albumsCount, data: $scope.albums}, photo: {count: $scope.photosCount, data: $scope.photos}};
HzPhotoService.setSharedData(data);
}
);
}
/**
* Get Photos data & count
* #returns {Array}
*/
$scope.doGetPhoto = function () {
return [{photos: $scope.photos, photoCount: $scope.photoCount}];
}
$scope.doEditDescription = function () {
console.log("description links from controller called");
}
angular
.module("AppWhizbite")
.controller('PhotoCtrl', ['$rootScope', '$scope', '$state', 'HzServices', 'HzPhotoService', PhotoCtrl]);
}());
photoList.html
<div>
<div class="total_album_photo gallery" ng-repeat-start="photo in photos track by $index">
<div class="no_of_photo imgWrapper">
<a href="javascript:void(0);" class="popup-link" data-index="{{$index}}">
<img ng-src="/resize/photo/{{photo.photo_name}}" height="120" width="120"/>
</a>
</div>
</div>
<div ng-repeat-end=""><hz-photo-popup></hz-photo-popup></div>
</div>
hzPhotoDirective.js
(function () {
'use strict';
angular
.module("AppWhizbite")
.directive("hzPhotoPopup", ["$rootScope", "$compile", "HzPhotoService", function ($rootScope, $compile, HzPhotoService) {
var magnificMarkup = "\n\
<form ng-controller=\"PhotoCtrl as Photo\" class=\"white-popup-block popMarkup ng-pristine ng-valid ng-scope\" id=\"dataPopup\" >\n\
<div class=\"popup_heading\">Photo</div>\n\
<div id=\"img_center\">\n\
<img style=\"width:100%\" src=\"\" id=\"img_center_content\" class=\"mfp-imageScale\">\n\
</div>\n\
<div class=\"popup_main\">\n\
<div class=\"popup_left photo_popup_left\">\n\
<div class=\"popup_raw1\">\n\
Edit description\n\
<div class=\"mfp-imageDescription\" style=\"cursor:pointer;\" ng-click=\"doEditDescription()\"></div>\n\
<textarea class=\"submitByEnter commentarea mfp-imageDescription\" placeholder=\"Edit description\" style=\"height: 76px;display:none;\"></textarea>\n\
</div>\n\
</div>\n\
</div>\n\
<div class=\"video_main\">\n\
</div>\n\
<button class=\"mfp-close\" type=\"button\" title=\"Close (Esc)\">×</button>\n\
</form>";
return {
restrict: "AE",
replace: false,
scope: true,
compile: function (scope, element) {
return{
pre: function (scope, element, attrs) {
if (scope.$last) {
// Iterate through all thumbnails class to bind magnific popup plugins
angular.forEach(angular.element(".gallery > .imgWrapper > a"), function (val, key) {
angular.element(".popup-link").eq(key).magnificPopup({
key: 'my-popup',
//items: arrData, // Array of media details
items: $rootScope.arrData, // Array of media details
index: key, // Index of media ref: data-index
type: 'inline',
verticalFit: true, // Fits image in area vertically
inline: {
// Magnific popup custom markup to show media (photo) gallery
markup: $compile(magnificMarkup)(scope)
},
gallery: {
enabled: true
},
callbacks: {
open: function () {
console.log("open called");
},
change: function () {
console.log("cahnge callaed");
},
markupParse: function (template, values, item) {
// optionally apply your own logic - modify "template" element based on data in "values"
// console.log('Parsing:', template, values, item);
console.log("markup parse called");
},
elementParse: function (item) {
console.log("element parse called");
}
}
});
});
}
}
}
},
link: function (scope, element, attrs) {
console.log("link method called");
}
}
}]);
}());
After R&D I crack the issue.
Magnific Popup callbacks objects have markupParse() method. It calls in every action of popup, so I put my angular js template $compile in markupParse method and it works fine.
It may different as per conditions or situations, but almost in all conditions it works finally fine.
Code:
inline: {
// Magnific popup custom markup to show media (photo) gallery
markup: magnificMarkup
},
callbacks:{
markupParse: function (template, values, item) {
$compile(template)(scope);
}
}
In the markupParse method having 3 parameters:
template : template holds actual HTML template which use in popup.
values: value holds current indexed value from arrData
item: item hold current item object.

Can there be an AngularJS directive to manage an array of conditions?

I'm new to AngularJS and my project at the moment has a menu that only needs to be displayed sometimes.
I therefore have:
<div class="iframe-hide"
ng-show="$state.includes('deposit.card.start')||
$state.includes('deposit.card.3ds')||
$state.includes('deposit.card.waiting')||
$state.includes('deposit.bank')||
$state.includes('deposit.x')||
$state.is('deposit.x.start')||
$state.is('deposit.y.start')||
$state.is('deposit.y.frame')">
As you can imagine, as the project grows this becomes unmanageable, so I want to look into tidying it up and creating perhaps a custom directive that will handle these conditions better.
I've been thinking of adding a custom data parameter like this:
.state("deposit.card.waiting", {
url: "/waiting",
templateUrl: "app/deposit/templates/card/waiting.html",
data: { includeMenu: true }
})
The Html instead would be:
<div class="iframe-hide" show-if-true="includeMenu">
And then a directive that will check whether includeMenu is true. I wrote it here:
export class showIfTrueDirective {
static $inject = ["$", "$rootScope"];
static $rootScope: any;
public static build($, $rootScope) {
var directive: ng.IDirective = {
link: (scope, element, attributes: any) => {
var itemToShow = attributes["showIfTrue"];
// this correctly prints "includeMenu"
// grab the data from current state?. If includeMenu == true then show element, otherwise hide element
}
};
return directive;
}
}
if I hook that up:
.directive("showIfTrue", ["$", "$rootScope", (r, s) => { return ShowIfTrueDirective.build(r,s); }])
If I manage to grab the scope data then this might work but this is my first week using Anglular and not entirely sure what I'm doing. Is there a better solution for this scenario?
I managed to solve it:
export class NgHideDirective {
static $inject = ["$rootScope"];
static $rootScope: any;
public static build($rootScope) {
var directive: ng.IDirective = {
link: (scope, element, attributes: any) => {
var itemToHide = attributes["ngHide"];
$rootScope.$on('$stateChangeStart',
(event, toState) => {
if (toState.data.hasOwnProperty(itemToHide)) {
element.hide();
} else {
element.show();
}
});
}
};
return directive;
}
}
So if we now do this on an element:
<div class="iframe" ng-hide="hideMenu">
And this on the state:
.state("deposit.x.rejected", {
url: "/rejected",
templateUrl: "app/deposit/templates/x/rejected.html",
data: { hideDepositMenu: null }
Then the div will be hidden.
However this doesn't work when page is refreshed for some reason.

Drag and drop ordering with ng-repeat

This is my html:
<div ng-repeat="activity in activities">
<button id="{{activity.id}}" activity=activity draggable>
{{activity.name}}
</button>
</div>
<div droppable handle="handleDrop(activity)">
<div ng-repeat="activity in getAll()">
<button>
{{activity.name}}
</button>
</div>
</div>
As you see I have two directives, draggable and droppable. The draggable directive sends the activity data to the droppable directive via dataTransfer. On the event 'drop', the data (the activity JSON) is added to a controller and getAll() returns all dropped activities. Now, this works. But I cannot change the order of these activities, since the drop only pushes the activity to a list. What should I do if I want to be able to put one activity in between two activities, or perhaps rearrange them?
EDIT:
app.directive('droppable', function () {
return {
scope: {
handle: '&'
},
link: function (scope, element) {
element.on('dragover', function (e) {
e.preventDefault();
});
element.on('drop', function (e) {
e.preventDefault();
var activity = JSON.parse(e.originalEvent.dataTransfer.getData("text"));
scope.handle({activity: activity});
scope.$apply();
});
}
};
});
app.directive('draggable', function () {
return {
scope: {
'activity' : '='
},
link: function (scope, element) {
var el = element[0];
el.draggable = true;
el.addEventListener('dragstart', function (e) {
e.dataTransfer.setData("text", JSON.stringify(scope.activity));
}, false);
}
}
});
I found a solution myself. What I did was to add this to the html draggable element:
data-index="{{$index}}"
Then, in the draggable directive in the dragenter event, I added:
if(e.target.hasAttribute('draggable')) {
e.target.classList.add('insert');
}
Where the insert class adds some styling to show that the user is about to drop between two elements. And in the drop event, I added:
var i = e.target.getAttribute('data-index');
if (i !== null) {
scope.handle({activity: activity, index: i});
} else {
scope.handle({activity: activity});
}
scope.$apply();
This solved it for me. Perhaps it could be useful to someone.

Refactoring. How to decouple Calls to Services and $scope values modification?

We are working with two models: Weeks and Days. Every Day belongs to a Week.
An we got two views: week-detail and day-detail
How could we decouple the Calls to Service and the $scope values modification?
(maybe using AngularJS Directives)
.
Week Detail View has the following template, there are listed all Days that belong to the specific Week
<!-- file 1: week-detail.tpl.html -->
<table>
<tr ng-repeat="day in weekdetail.days">
<td>
<span ng-click="toggleActive(day.id, $index)">
{{ day.active }}
</span>
</td>
</tr>
</table>
The Controller that commands this View is the following:
// file 2: week-detail.controller.js
function WeekDetailCtrl ($scope, $routeParams, Week, Days, DayToggleActive) {
this.week = Week.get({ id: $routeParams.id });
this.days = Days.query({ id: $routeParams.id });
// #param dayId: id to be processed by DayToggleActvive.toggleActive() service
// #param position: $index from ng-repeat, to modify this specific DOM element
$scope.toggleActive = function(dayId, position) {
// call to service
(DayToggleActive.toggleActive({ id: dayId }))
.$promise
.then(function(data) {
// $scope values manipulation
$scope.weekdetail.week = data;
$scope.weekdetail.days[position].active = !$scope.weekdetail.days[position].active;
});
};
}
Day Detail View has the following template:
<!-- file 3: day-detail.tpl.html -->
<div ng-click="toggleDetailActive(daydetail.day.id)">
{{ daydetail.day.active }}
</div>
The Controller that commands this View is the following:
// file 4: day-detail.controller.js
function DayDetailCtrl ($scope, $routeParams, Day, DayToggleActive) {
this.day = Day.get({ id: $routeParams.id });
this.ods = Ods.query({ id: $routeParams.id });
// #param dayId: id to be processed by DayToggleActvive.toggleActive() service
$scope.toggleDetailActive = function(dayId) {
// call to same service
(DayToggleActive.toggleActive({ id: dayId }))
.$promise
.then(function(data) {
// $scope value manipulation is different in Day Detail View
$scope.daydetail.day.active = !$scope.daydetail.day.active;
});
};
}
.
Thank you in advance for your help
UPDATE
Thanks to help of Oddman, we have proceeded with an advance
We have added a Custom Directive Tag to HTML
<!-- file 3: day-detail.tpl.html -->
<div toggleActive="daydetail.day">
{{ daydetail.day.active }}
</div>
And added the directive to module
// file 4: day-detail.controller.js
module.directive('toggleActive', toggleActive);
function toggleActive() {
return {
scope: {day: '=toggleActive'},
restrict: "A",
link: function(scope, element, attrs ) {
element.bind("click", function() {
scope.day.active = !scope.day.active;
});
}
}
}
That looks better, but... what we need is to toggle scope.day.active after successfully call to Service, so...
// file 4: day-detail.controller.js
// inject DayToggleActive service dependency
module.directive('toggleActive', ['DayToggleActive', toggleActive]);
function toggleActive(DayToggleActive) {
return {
scope: {day: '=toggleActive'},
restrict: "A",
link: function(scope, element, attrs ) {
element.bind("click", function() {
(DayToggleActive.toggleActive({ id: scope.day.id }))
.$promise
.then(function(data) {
scope.day.active = !scope.day.active;
})
.catch(function(response) {
console.log(response);
});
});
}
}
}
Previously, the reference was in controller declaration, so let's remove it
// file 4: day-detail.controller.js
function DayDetailCtrl ($scope, $routeParams, Day) {
...
NOW
Week Detail View can be refactored the same way
<!-- file 1: week-detail.tpl.html -->
<table>
<tr ng-repeat="day in weekdetail.days">
<td>
<span toggleDayActive="day">
{{ day.active }}
</span>
</td>
</tr>
</table>
And Day Detail Controller will include a similar directive to which we defined for Week Detail Controller
// file 2: week-detail.controller.js
// inject DayToggleActive service dependency
module.directive('toggleDayActive', ['DayToggleActive', toggleDayActive]);
function toggleDayActive(DayToggleActive) {
return {
scope: {day: '=toggleDayActive'},
restrict: "A",
link: function(scope, element, attrs ) {
element.bind("click", function() {
(DayToggleActive.toggleActive({ id: scope.day.id }))
.$promise
.then(function(data) {
scope.day.active = !scope.day.active;
// HOW COULD WE ACCESS PREVIUSLY REFERENCED AS $scope.weekdetail.week ?
})
.catch(function(response) {
console.log(response);
});
});
}
}
}
How could we regain access to previously referenced as $scope.weekdetail.week ?
Do we need to pass that as parameter in an way?
Is there a way to reach that $scope?
I would setup directives for your toggling, and just pass the day or week to that. For example:
<div toggle-week="week"></div>
Then in your directive, something like:
module.directive('toggleWeek', function() {
return {
scope: {week: '=toggleWeek'},
link: function(scope, element, attrs) {
// add your scope function here
}
};
});
You can do a similar thing for your daily toggle. Then you just can just deal with the week/day binding in your directive rather than working directly on the $scope.
Hope that helps.

Resources