HI my goal is to use Observable sequence to listen to dragEnter/dragleave events to change css classes. IE :
var myDraggableListEl = document.querySelector....
var itemDragIn$ = Rx.Observable.fromEvent(myDraggableListEl, 'dragenter').map((e)=> e.target);
var itemDragOut$ = Rx.Observable.fromEvent(myDraggableListEl, 'dragleave').map((e)=> e.target);
Basically I just want to add a 'hover' class on 'dragenter',and then remove the same class on dragleave.
I was wondering if there was a clever way to 'merge' these 2 so that the subscription would just be something like:
.subscribe( (boolAdd, className, target ) => {
boolAdd ? target.classlist.add(className): target.classlist.remove(className)
}
Maybe Im just overthinking this andI just leave the two observables as separate, but just wondering if anyone had accomplished this or if they had a better way.
Thanks!
You can totally do this using the Rx.Observable.merge() method.
Links:
merge - general rx
merge - rxjs example
Example (Here's the Fiddle):
Rx.Observable.merge(
Rx.Observable.fromEvent($el, 'dragenter').map(function(e) {
return { target: e.target, isAdd: true, className: 'dragg' }
}),
Rx.Observable.fromEvent($el, 'dragleave').map(function(e) {
return { target: e.target, isAdd: false, className: 'dragg' }
}))
.subscribe(function(obj){
if(obj.isAdd) {
obj.target.classlist.add(obj.className);
} else {
obj.target.classlist.remove(obj.className);
}
});
Although please consider that the following is probably much more readable:
// addClass and removeClass are defined somewhere (can represent jquery's functions)
Rx.Observable
.fromEvent($el, 'dragenter')
.subscribe(function(e){
addClass(e.target, 'dragg');
});
Rx.Observable
.fromEvent($el, 'dragleave')
.subscribe(function(e){
removeClass(e.target, 'dragg');
});
Related
I`m building third party application for specific sites with Jquery.
Recently I started to use rx.Observable in my project. However, I found to use of this new JS library sometimes is hard to understand. I have tried to convert next peace of code to use with Observables, but it is not working at all;
class EventsUtils {
constructor() {
this.observable = Rx.Observable;
}
bindUserLeavePageEvent() {
var self = this;
document.addEventListener('mouseleave', (e) => {
$JQ(document).trigger('mouseleave.mo');
}, false);
/*We cannot remove document mouse over event thus we trigger Jquery registered custom event and on remove we cancel it*/
$JQ(document).off('mouseleave.mo').on('mouseleave.mo', (e) => {
if (e.clientY < 0 && !self.loaded) {
console.log('loading from screen Leave');
$JQ('.fixed-button').trigger('click');
self.loaded = true;
}
});
}
$JQ variable is came from jquery.noConflict due to i am running not on my page.
To convert second expression to Observable I have tried to use next statement:
this.observable.fromEvent(document, 'mouseleave.mo').pluck('currentTarget').subscribe(x=>console.log(x));
}
But without success.
How to convert above event statements to use with Observable and what is common pattern to do this;
It seems as if jquery.trigger does not really work with custom events - you can only catch those events through $(elem).on as they are handles internally for browser-compatibility-reasons. (https://github.com/jquery/jquery/issues/2476)
But you can relatively easy dispatch custom events (unless you want to target IE<=8)
document.addEventListener("mouseleave", () => {
console.log("Original event: Leave");
// dispatching custom events with vanilla-js (should work all the way down to IE9)
const event = document.createEvent("CustomEvent");
event.initEvent("mo.leave", true, true);
document.dispatchEvent(event);
});
Rx.Observable
.fromEvent(document, "mo.leave")
.pluck("currentTarget")
.subscribe(target => console.info("Target is", target.nodeName));
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>
I am using $.observable(array).insert() to append items to a list. This is updating my view as it should: new list items are rendered to the DOM. However, I would like to issue a click event on the new DOM node (I'm relying on the event to add a class to expand the item and attach another listener to the body so the area can be closed).
I have tried both
$.observable(_model.leadTimes).insert(leadTime);
$leadTimes.find('.lead-time-data').last().find('.start-editing').click();
...and
function watchLeadTimes() {
var changeHandler = function (ev, eventArgs) {
if (eventArgs.change === 'insert') {
$leadTimes.find('.lead-time-data').last().find('.start-editing').click();
}
};
$.observe(_model.leadTimes, changeHandler);
}
And neither of them worked, however, if I wrap the jQuery method in a setTimout, like setTimeout(function () { $leadTimes.find('.lead-time-data').last().find('.start-editing').click(); }, 400);, it does work, leading me to believe this is an issue of timing with the DOM render somehow not finishing before my jQuery click() method is invoked.
Since the odds are decent that you will see this, Borris, thank you for the library and all that you do! I think jsViews is an excellent middle ground between the monolithic frameworks out there and plain old jQuery noodling!
Edit 02/09/17
It turns out my issue was overlapping click events--I was inadvertently handling a click to deselect my element immediately after it was selected. However I took the opportunity to rewrite things to use a more declarative approach following Borris' linked example.
Now in my template I am using a computed observable, isSelected to toggle the .editing class:
{^{for leadTimes}}
<tr class="lead-time-data" data-link="class{merge:~isSelected() toggle='editing'}">
<span>{^{:daysLead}}</span>
</tr>
{{/for}}
And this JS:
function addNewLeadTimeClickHandler() {
var onNewLeadTimeClick = function () {
e.stopPropagation(); // this is what I was missing
var leadTime = {
daysLead: 1,
description: ''
};
$.observable(_model.activityMapping.leadTimes).insert(leadTime);
selectLeadtime(_model.activityMapping.leadTimes.length -1);
}
$leadTimes.on('click', '.add', onNewLeadTimeClick);
}
function selectLeadtime(index) {
var addStopEditingClickHandler = function () {
var onClickHandler = function (event) {
if ($(event.target).closest('tr').hasClass('editing')) {
setHandler();
return;
}
selectLeadtime(-1)
};
function setHandler() {
var clickEvent = 'click.ActivityChangeRequestDetailController-outside-edit-row';
$('html:not(.edit)').off(clickEvent).one(clickEvent, onClickHandler);
};
setHandler();
}
if (_model.selectedLeadtimeIndex !== index) {
$.observable(_model).setProperty('selectedLeadtimeIndex', index)
addStopEditingClickHandler();
}
}
function isSelected() {
var view = this;
return this.index === _model.selectedLeadtimeIndex;
}
// isSelected.depends = ["_model^selectedLeadtimeIndex"];
// for some reason I could not get the above .depends syntax to work
// ...or "_model.selectedLeadtimeIndex" or "_model.selectedLeadtimeIndex"
// but this worked ...
isSelected.depends = function() {return [_model, "selectedLeadtimeIndex"]};
The observable insert() method is synchronous. If your list items are rendered simply using {^{for}}, then that is also synchronous, so you should not need to use setTimeout, or a callback. (There are such callbacks available, but you should not need them for this scenario.)
See for example http://www.jsviews.com/#samples/editable/tags (code here):
$.observable(movies).insert({...});
// Set selection on the added item
app.select($.view(".movies tr:last").index);
The selection is getting added, synchronously, on the newly inserted item.
Do you have other asynchronous code somewhere in your rendering?
BTW generally you don't need to add new click handlers to added elements, if you use the delegate pattern. For example, in the same sample, a click handler to remove a movie is added initially to the container "#movieList" with a delegate selector ".removeMovie" (See code). That will work even for movies added later.
The same scenario works using {{on}} See http://www.jsviews.com/#link-events: "The selector argument can target elements that are added later"
I got the following binding working like a charm :
<button class="flatButton buttonPin" data-bind="click:EnterPinMode">Add pin</button>
In my viewmodel I define the Handler like this :
self.EnterPinMode = function(data,event)
{
//Doing several things here
//....
}
Now, let's say I want to change the behavior of that button after the first click on it...how could I do it ? I already managed quite easily to change the button text :
self.EnterPinMode = function(data,event)
{
//Doing several things here
//....
var curButton = $(event.target);
curButton.text("Cancel");
}
But what about changing the button behaviour ? If I had set this handler through jQuery, that wouldn't be an issue, but is there a way to "replace" the click binding on that control so that now it will call ExitPinMode handler, for example.
I've got some doubts on this being possible given the fact that knockout works only with declarative binding (at least without plugin...), but I thought it was worth asking.
Please note that I will actually need some kind of 3 ways toggle, I just simplify it here to a "normal" toggle for the sake of the example.
I think using a hasBeenClicked flag that's private to the view model is fine, and probably the best solution for this.
If you really want to swap out the handler, that should be easy enough, though, with something like this:
function enterPinMode() {
//Doing several things here
//....
var curButton = $(event.target);
curButton.text("Cancel");
//set click handler to a step 2 function
self.pinAction = exitPinMode;
}
function exitPinMode() {
//....
}
self.pinAction = enterPinMode;
Maybe one of the simplest solution is to add a boolean like hasBeenClicked set to false at the begining and then set it to true.
Example:
self.hasBeenClicked = false;
self.EnterPinMode = function(data,event)
{
if (!self.hasBeenClicked )
{
var curButton = $(event.target);
curButton.text("Cancel");
self.hasBeenClicked = true;
}
else
{
//behaviour an a second click
}
}
Hope it helps !
You can try this
var vm = function () {
var self = this;
var nextState = 'firstState';
var states = {
firstState: function () {
nextState = 'secondState';
//Do stuff
},
secondState: function () {
nextState = 'thirdState';
//Do stuff
},
thirdState: function () {
nextState = 'firstState';
//Do stuff
}
}
self.EnterPinMode = function () {
states[nextState].call();
}
}
What you should try to remember first about MVVM is that you are designing an object to represent your view. If your view will have different states, There is nothing wrong with having your viewmodel know about these states and knowing what to do in what state. Stick with MVVM. You wont be disappointed.
Ok this is my first stab at creating a jQuery plugin so I am going off tutorials currently.
This far I have
(function($)
{
$.tippedOff = function(selector, settings)
{
var config = {
'top':0,
'left':0,
'wait':3000
};
if(settings){$.extend(config, settings);}
var $elem = $(selector);
if($elem.length > 0)
{
$elem.each(function()
{
$(this).css({"color":"#F00"});
})
}
return this;
};
})(jQuery);
Which works for changing the text color of the provided elements. However. I want to add functionality to elements that the plugin takes effect on. Such as a hover or click event. But I can't wrap my head around that idea at the moment, seeing as the selector can be anything. So its not like I can hardcode something in per say thats specific like I would through normal jQuery methods.
So, with that, how do I go about adding that type of functionality to things after its been rendered?
When creating plugins, it is very easy to over-complicate things, so try to keep things nice and simple.
I have provided you with TWO examples of the tippedOff plugin. Here is also a jsfiddle demo of both plugins.
The first uses your original code as is (NO SIGNIFICANT CHANGES MADE):
$.tippedOff = function(selector, settings)
{
var config = {
'top':0,
'left':0,
'wait':3000
};
if(settings){$.extend(config, settings);}
var $elem = $(selector);
if($elem.length > 0)
{
$elem.each(function()
{
//bind mouseenter, mouseleave, click event
$(this).css({"color":"#F00"})
.mouseenter(function(){
$(this).css({"color":"green"});
})
.mouseleave(function(){
$(this).css({"color":"#F00"});
})
.click(function(){
$(this).html('clicked');
});
})
}
return this;
};
This one, however, is based on your original code. Basically, I have reconstructed your original code using these tips. This is how I would personally go about it. I have also provided you with a breakdown below of changes made. (SIGNIFICANT CHANGES MADE):
$.fn.tippedOff = function(settings) {
var config = $.extend( {
'top':0,
'left':0,
'wait':3000,
'color': 'orange',
'hoverColor': 'blue'
}, settings);
return this.each(function() {
$this = $(this);
$this.css({ 'color': config.color})
.mouseenter(function(){
$this.css({ 'color': config.hoverColor });
})
.mouseleave(function(){
$this.css({ 'color': config.color });
})
.click(function(){
$this.html('clicked');
});
});
}
----------------------------------------
Breakdown:
Original Code:
$.tippedOff = function(selector, settings) {
Changed:
$.fn.tippedOff = function( settings ) {
Comments:
The difference between $.tippedOff and $.fn.tippedOff is huge! Adding your plugin to the $.fn namespace rather than the $ namespace will prevent you from having to provide a selector and makes life simplier.
I personally like this answer, in which #Chad states:
My rule of thumb I follow is: use $. when it is not DOM related (like ajax), and use $.fn. when it operates on elements grabbed with a selector (like DOM/XML elements).
Original Code:
if(settings){$.extend(config, settings);}
Changed:
var config = $.extend( {
'top':0,
'left':0,
'wait':3000
}, settings);
Comments:
Having an if statement is redundant. .extend() does all the work for you.
Original Code:
var $elem = $(selector);
if($elem.length > 0)
{
$elem.each(function()
{
$(this).css({"color":"#F00"});
})
}
return this;
Changed:
return this.each(function() {
$this = $(this);
$this.css({ 'color': config.color});
});
Comments:
Using return this.each(function(){}) is good practice and maintains chainability. Not only that, you will no longer need to worry about the selector's length.
*NOTE: If you want to add additional events, then use different methods within your plugin: jQuery Doc Reference - Authoring Plugins.
I hope this helps and please let me know if you have any questions!
I have not enough reputation points to comment and fully agree with Dom, who is very knowledgeable. I only would like to add that within the changed code it would be better to create a local variable by using the var keyword:
var $this = $(this);
This will make the plugin better and allows you to apply the plugin to multiple elements one the page as for example:
$('#testX').tippedOff2();
$('#testY').tippedOff2();
$('#testZ').tippedOff2();
I'm building a simple jQuery plugin called magicForm (How ridiculous is this?). Now face to a problem that I think I'm not figuring out properly.
My plugin is supposed to be applied on a container element, that will show each of its inputs one by one as user fills them. That's not the exact purpose of my problem.
Each time I initialize the container, I declare an event click callback. Let me show an example.
(function($){
var methods = {
init: function(options){
return this.each(function(){
var form, inputs;
var settings = {
debug: false
};
settings = $.extend(settings, options);
form = $(this);
$('a.submit', form).on('click', function(event){
if (settings.submitCallback) {
settings.submitCallback.call(form, inputs);
}
return false;
});
});
},
reset: function() {
}
}
$.fn.magicForm = function(method) {
if ( methods[method] ) {
return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof method === 'object' || ! method ) {
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + method + ' does not exist.' );
}
};
})($);
I'm focusing on a specific part of this code :
$('a.submit', form).on('click', function(event){
if (settings.submitCallback) {
settings.submitCallback.call(form, inputs);
}
return false;
});
Because each time the init method is called, that poor callback is registered.
I was experiencing this painfully, when I invoked my plugin on an element nested in a twitter bootstrap 'tab', nested itself in a bootstrap modal :
I was calling init each time the event 'shown' of my bootstrap modal was triggered.
So, this is how I fixed it in my init method :
// Prevent callback cumulation
if (!$(this).data('form_initialized')) {
$('a.submit', form).on('click', function(event){
if (settings.submitCallback) {
settings.submitCallback.call(form, inputs);
}
return false;
});
$(this).data('form_initialized', true);
}
And I'm far from feeling sure about this.
Thank your for your time !
Many jquery plugins use data to know if their plugins were initialized. Most often, they use the name of their own plugin as a part (or in whole) as the data. For example:
$(this).data('magicForm')
So your approach of using that to signal is not a bad one.
However, you have two other options:
1) Pull the event handler out so the handler is a single instance. Above your methods, do var fnOnSubmit = function() { ... } Then you can simply ensure proper binding by calling $('a.submit', form).unbind('click', fnOnSubmit) before rebinding it the way you are already doing it.
2) Another option is to use event namespaces.
$('a.submit', form).unbind('click.magicForm'); then rebinding it with .on('click.magicForm') This namespace approach ensures that when you unbind it only unbinds in the context of your namespace magicForm, thus leaving all other click events (e.g. from other plugins) intact.
I hope this helps.
You could first explicitely remove the click-handler:
$('a.submit', form).off('click').on('click', function(event){ ... })
However, I would suggest you use event namespacing to prevent all click handlers (even those perhaps set by code not your own) from being removed:
$('a.submit', form).off('click.magicForm').on('click.magicForm', function(event){ ... })