I'm just working on a projet in Angular 1. The task is to do a DOM tree from json object and manipulate with it through native drag'n'drop DOM operations (with out jQuery and etc). I've been done parsing json-> dom function, loading json from server, some drag'n'drop event handlers. Now my question is how to update a json object (from this object I get a ul>li tree structure) when I done a DROP event.
Now, code
View
<div ng-controller="TreeController">
<ng-dom-tree
style="background-color: #000"
ng-model = "treeJson"
class="tree"
ng-draggable
ng-droppable>
</ng-dom-tree>
</div>
Controller
.controller('TreeController', TreeController);
TreeController.$inject = ['treeService', '$scope'];
function TreeController(treeService, $scope) {
$scope.treeJson = '';
treeService.getTree().success(function (data) {
$scope.treeJson = data;
});
}
Main directive
.directive('ngDomTree', ngDomTree)
ngDomTree.$inject = [];
function ngDomTree() {
var isEmpty = function (object) {
for (var key in object) {
return false;
}
return true;
};
function createTree(tree, list) { /*creating tree -> json to dom*/}
return {
restrict: 'E',
replace: true,
link: function (scope, elt, attrs) {
scope.$watch('treeJson', function (data) {
if (isEmpty(data))
return;
**CAN'T HANDLE DATA CHANGING HERE**
elt.append(document.createElement('ul'));
createTree(data, document.getElementsByTagName('ul')[0]);
});
}
}
}
Sup directive
.directive('ngDroppable', ngDroppable)
ngDroppable.$inject = [];
function ngDroppable() {
var parseTreeToJson = function(tree){/* dom to json */}
return {
restrict: 'A',
link: function (scope, elt, attrs) {
elt.on('mouseover', function (e) {
var droppableElt = e.target || event.target;
if (!droppableElt.classList.contains('tree__node') && !droppableElt.classList.contains('tree__branch'))
return;
droppableElt.addEventListener(
'dragover',
function (e) {
this.classList.add('navigator');
e.dataTransfer.dropEffect = 'move';
if (e.preventDefault)
e.preventDefault();
this.classList.add('over');
return false;
},
false
);
droppableElt.addEventListener(
'dragenter',
function (e) {
this.classList.add('over');
return false;
},
false
);
droppableElt.addEventListener(
'dragleave',
function (e) {
this.classList.remove('over');
this.classList.remove('navigator');
return false;
},
false
);
droppableElt.addEventListener(
'drop',
function (e) {
if (e.stopPropagation) e.stopPropagation();
this.classList.remove('over');
let item = document.getElementById(e.dataTransfer.getData('Text'));
this.appendChild(item);
item.id = '';
//updating here
scope.treeJson = parseTreeToJson(elt[0].children[0]);
return false;
},
false
);
});
}
}
}
So, In Sup directive when drop event created, and I'm reinit treeJson variable, after that I need in main directive reinitializing the tree and also in controller get new json structure from this variable, because $watch is used, but it isn't happened.
PLEASE HELP
THNKS FOR ATTENTION :)
P.S. Here it is in Plnkr.co
Since you are using native DOM, it bypasses angular's processors. You need to call scope.$digest() after changing angular's state to tell it that something changed.
droppableElt.addEventListener(
'drop',
function (e) {
if (e.stopPropagation) e.stopPropagation();
this.classList.remove('over');
let item = document.getElementById(e.dataTransfer.getData('Text'));
this.appendChild(item);
item.id = '';
scope.treeJson = parseTreeToDOM(elt[0].children[0]);
scope.$digest();
},
false
);
Related
I have AngularJS directive like this:
(function () {
'use strict';
// better click that ingore drag
angular
.module('module')
.directive('exClick', exClick);
exClick.$inject = [];
function exClick() {
return {
restrict: 'A',
scope: {
exClick: '&'
},
link: function ($scope, $element) {
var isDragging = false;
function mousemove() {
isDragging = true;
$(window).off('mousemove', mousemove);
}
var timer;
$element.mousedown(function() {
isDragging = false;
// there is wierd issue where move is triggerd just
// after mousedown even without moving the cursor
timer = setTimeout(function() {
$(window).mousemove(mousemove);
}, 100);
}).mouseup(function(e) {
var wasDragging = isDragging;
isDragging = false;
clearTimeout(timer);
$(window).off('mousemove', mousemove);
if (!wasDragging) {
$scope.$apply($scope.exClick);
}
});
$scope.$on('$destroy', function() {
$(window).off('mousemove', mousemove);
$element.off('mousedown mouseup');
});
}
}
}
})();
and I want to use like normal ng event I have ng-click on table row and on row controls I have ng-click="$event.stopPropagation()". I've replaced row with ex-click and I want to use ex-click="$event.stopPropagation()". I can use ng-mouseup to prevent the event from happening, but I want to know how to make my custom event to behave the same as native ng event.
I've tried:
$scope.exClick({$event: e});
and
$scope.$event = e;
$scope.$apply();
$scope.exClick();
Found in the source code ngClick is kind of normal AngularJS directive (it's added differently but it have the same compile and link, so it should work the same).
And for reference here is the copy of the code:
forEach(
'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
function(eventName) {
var directiveName = directiveNormalize('ng-' + eventName);
ngEventDirectives[directiveName] = ['$parse', '$rootScope', '$exceptionHandler', function($parse, $rootScope, $exceptionHandler) {
return createEventDirective($parse, $rootScope, $exceptionHandler, directiveName, eventName, forceAsyncEvents[eventName]);
}];
}
);
function createEventDirective($parse, $rootScope, $exceptionHandler, directiveName, eventName, forceAsync) {
return {
restrict: 'A',
compile: function($element, attr) {
// NOTE:
// We expose the powerful `$event` object on the scope that provides access to the Window,
// etc. This is OK, because expressions are not sandboxed any more (and the expression
// sandbox was never meant to be a security feature anyway).
var fn = $parse(attr[directiveName]);
return function ngEventHandler(scope, element) {
element.on(eventName, function(event) {
var callback = function() {
fn(scope, {$event: event});
};
if (!$rootScope.$$phase) {
scope.$apply(callback);
} else if (forceAsync) {
scope.$evalAsync(callback);
} else {
try {
callback();
} catch (error) {
$exceptionHandler(error);
}
}
});
};
}
};
}
I'm trying to capture if the enter key has been pressed and execute a search. This is the viewmodel for the search page.
(function ()
{
a.viewModels.userSearch = function (view, params) {
$view = $(view);
var self = a.viewModel({
users: a.collection({
url: '/admin/Account/SearchUsers',
query: {
SearchText: null
}
}).fetch(),
setPageIndex: setPageIndex,
search: search
});
$view.keypress(function (e) {
if (e.keyCode == 13) {
self.search(e);
}
});
function search(e) {
self.users.query.rowCount = 0;
self.users.query.pageIndex = 1;
self.users.fetch();
}
function setPageIndex(e) {
e.preventDefault();
self.users.query.set('pageIndex', $(e.currentTarget).data('page-index'));
self.users.fetch();
}
return self;
}
Now, this works. The problem is that it works only after pressing the 'Enter' key 2 times. Seems like I'm missing something related to the scope but js ain't my cup of tea.
If it is of any help, here goes my view model function:
function viewModel(viewModelConfig) {
var self = kendo.observable($.extend({
busy: 0,
resultMessage: null,
clearResultMessage: clearResultMessage
}, viewModelConfig));
self.bind('change', onChange);
function onChange(change) {
var errorProp, errorMsg, infoProp, infoMsg;
if (change.field.endsWith('.busy')) {
if (self.get(change.field))
self.set('busy', self.busy + 1);
else if (self.busy > 0)
self.set('busy', self.busy - 1);
}
else if (change.field.endsWith('.resultMessage')) {
var data = self.get(change.field);
self.set('resultMessage', data);
}
}
function clearResultMessage(e)
{
if (e) e.preventDefault();
self.set('resultMessage', null);
return false;
}
return self;
}
I have a similar setup on my site, and using MVVM, just add the custom enter binding within the data-bind attribute of the element to link to the function within the view-model you wish to execute.
The code to add the custom binder is as such:
kendo.data.binders.widget.enter = kendo.data.Binder.extend({
init: function(element, bindings, options) {
kendo.data.Binder.fn.init.call(this, element, bindings, options);
var binding = this.bindings.enter;
$(element.element).keyup(function(e) {
if( e.which === 13 )
bindings.get();
});
},
refresh: $.noop
});
In order to get this ability i have extended tooltip provider.
function customTooltip($document, $tooltip) {
var tooltip = $tooltip('customTooltip', 'customTooltip', 'click'),
parentCompile = angular.copy(tooltip.compile);
tooltip.compile = function (element, attrs) {
var parentLink = parentCompile(element, attrs);
return function postLink(scope, element, attrs) {
var firstTime = true;
parentLink(scope, element, attrs);
var onDocumentClick = function () {
if (firstTime) {
firstTime = false;
} else {
element.triggerHandler('documentClick');
}
};
var bindDocumentClick = function () {
$document.on('click', onDocumentClick);
};
var unbindDocumentClick = function () {
$document.off('click', onDocumentClick);
};
scope.$watch('tt_isOpen', function (newValue) {
firstTime = true;
if (newValue) {
bindDocumentClick();
} else {
unbindDocumentClick();
}
});
scope.$on('$destroy', function onTooltipDestroy() {
unbindDocumentClick();
});
};
};
return tooltip;
}
But this approach doesn't work already because there is no tt_isOpen property in scope now. Actually i can't see any of tooltip properties just only my parent scope. I guess this happend because of changes in tooltip.js 124 line https://github.com/angular-ui/bootstrap/blob/master/src/tooltip/tooltip.js#L124. Is there any way now to close tooltip by clicking outside it or at least to get isOpen flag?
There is a pull request that implements an outsideClick trigger for tooltips and popovers. It will be included in angular-ui 1.0.0, which is expected to be released by the end of the year. Once it is implemented, you will be able to simply add tooltip-trigger="outsideClick" to your element.
There is an open pull request Here to add this feature. A hack workaround you can try is to disable then enable the trigger element as the directive will call this method:
attrs.$observe( 'disabled', function ( val ) {
if (val && ttScope.isOpen ) {
hide();
}
});
This variant works on angular 1.3.15 and angular-ui version 0.13
function customTooltip($document, $tooltip) {
var tooltip = $tooltip('customTooltip', 'customTooltip', 'click'),
parentCompile = angular.copy(tooltip.compile);
tooltip.compile = function (element, attrs) {
var parentLink = parentCompile(element, attrs);
return function postLink(scope, element, attrs) {
parentLink(scope, element, attrs);
var isOpened = false;
element.bind('click', function () {
bindDocumentClick();
});
var onDocumentClick = function () {
if (!isOpened) {
isOpened = true;
} else {
element.triggerHandler('documentClick');
unbindDocumentClick();
isOpened = false;
}
};
var bindDocumentClick = function () {
$document.on('click', onDocumentClick);
};
var unbindDocumentClick = function () {
$document.off('click', onDocumentClick);
};
scope.$on('$destroy', function onTooltipDestroy() {
unbindDocumentClick();
});
};
};
return tooltip;
}
I'm trying to create drag and drop directives for angularJS by following this post: http://jasonturim.wordpress.com/2013/09/01/angularjs-drag-and-drop/
But I can not send any data because inside the dragstart event handler dataTransfer and the originalEvent are always null.
My drag directive looks like this:
directives.directive('draggSrc', ['$rootScope', 'UuidService', function ($rootScope, UuidService) {
var directive = {};
directive.restrict = 'A';
directive.link = function (scope, el, attrs) {
el.attr("draggable", "true");
var id = attrs.id;
if (!attrs.id) {
id = UuidService.new();
el.attr("id", id);
}
el.bind("dragstart", function (evt) {
console.log(evt);
console.log(evt.dataTransfer);
console.log(evt.originalEvent);
evt.dataTransfer.setData('text', id);
$rootScope.$emit("DRAG-START");
});
el.bind("dragend", function (evt) {
$rootScope.$emit("DRAG-END");
});
};
return directive;
}]);
I have also called $.event.props.push('dataTransfer').
In order to solve my problem I have switched from using jQuery's bind to using JavaScript's addEventListener and everything worked as expected:
directives.directive('draggSrc', ['$rootScope', 'UuidService', function ($rootScope, UuidService) {
var directive = {};
directive.restrict = 'A';
directive.link = function (scope, el, attrs) {
el.attr("draggable", "true");
var id = attrs.id;
if (!attrs.id) {
id = UuidService.new();
el.attr("id", id);
}
el.get(0).addEventListener("dragstart", function (evt) {
evt.dataTransfer.setData('text', id);
$rootScope.$emit("DRAG-START");
});
el.get(0).addEventListener("dragend", function (evt) {
$rootScope.$emit("DRAG-END");
});
};
return directive;
}]);
Had this same issue with a different fix: if you have an alert() or confirm() dialog in your drop handler, that clears out the event.dataTransfer.
In the following implementation of a hypothetical navigation module the module object returns properties such as isOverBinded or isNavTurnedOff which basically return the consequential value of other methods.
This methods are then utilised in the test cases to check whether a property call has caused its expected consequence.
Should these methods be kept or the original method in question return the consequential values and the same method to be used in the test case?
Currently the code is:
var navModule = (function(element) {
var nav = {};
var navHTMLobjs = {
navList : element,
listItems : element.find('li'),
listLinks : element.find('a')
};
nav.bindOver = function() {
navHTMLobjs.navList.on('mouseover mouseout', 'li a', function(e) {
if (e.type == 'mouseover') {
$(this).addClass('over');
}
if (e.type == 'mouseout') {
$(this).removeClass('over');
}
});
};
nav.isOverBinded = function(){
return navHTMLobjs.navList.data('events').hasOwnProperty('mouseover')
&& navHTMLobjs.navList.data('events').hasOwnProperty('mouseout');
};
nav.turnOff = function() {
navHTMLobjs.navList.off('mouseover mouseout');
};
nav.isNavTurnedOff = function() {
return !navHTMLobjs.navList.data.hasOwnProperty('events');
};
nav.init = function() {
this.bindOver();
};
return nav;
});
var myNav = new navModule($('#nav'));
/// Test cases:
module('Navigation module');
test('Binding total', function() {
myNav.init();
equal(myNav.isOverBinded(), true, "Does the init function attach all events?");
});
test('Unbinding total', function() {
myNav.turnOff();
equal(myNav.isNavTurnedOff(), true, "Does the cancel function correctly unbind events?");
});
For example should I change nav.bingOver to be:
nav.bindOver = function() {
navHTMLobjs.navList.on('mouseover mouseout', 'li a', function(e) {
if (e.type == 'mouseover') {
$(this).addClass('over');
}
if (e.type == 'mouseout') {
$(this).removeClass('over');
}
});
return navHTMLobjs.navList.data('events').hasOwnProperty('mouseover')
&& navHTMLobjs.navList.data('events').hasOwnProperty('mouseout');
};
...and then use the same method in the test case like below?
test('Binding total', function() {
myNav.init();
equal(myNav.bindOver(), true, "Does the init function attach all events?");
});
What are the differences between the two?
Many thanks
Assuming other parts of the app don't need to independently verify whether the events have been subscribed to, the bindOver() should not return any value. Also, the isOverBinded() doesnt belong to the navigation module. Its existence is purely to help implement the test. In such a case, that function should be within the testing suite.
var navModule = (function(element) {
var nav = {};
var navHTMLobjs = {
navList : element,
listItems : element.find('li'),
listLinks : element.find('a')
};
nav.bindOver = function() {
navHTMLobjs.navList.on('mouseover mouseout', 'li a', function(e) {
if (e.type == 'mouseover') {
$(this).addClass('over');
}
if (e.type == 'mouseout') {
$(this).removeClass('over');
}
});
};
nav.turnOff = function() {
navHTMLobjs.navList.off('mouseover mouseout');
};
nav.init = function() {
this.bindOver();
};
return nav;
});
//var myNav = new navModule($('#nav'));
/// Test cases:
module('Navigation module');
// you might already have such a in memory object
$root = $('<ul></ul>').append('<li></li><li></li>');
var myNav = new navModule($root);
test('Binding total', function() {
myNav.init();
equal(isOverBinded(), true, "Does the init function attach all events?");
});
test('Unbinding total', function() {
myNav.turnOff();
equal(isNavTurnedOff(), true, "Does the cancel function correctly unbind events?");
});
var isNavTurnedOff = function() {
return $root.data('events').hasOwnProperty('mouseover') && $root.data('events').hasOwnProperty('mouseout');
}
var isOverBinded = function() {
return $root.data.hasOwnProperty('events') === false;
}
At the end of the day I feel, whether or not the function ought to return a value should depend on the usage of the function and not for making testing easier.