In Solid, why does this effect not re-run when count is updated? After some tinkering, I've found that it has to with count being in the setTimeout callback function, but what's the intuitive way to understand what things inside an effect are tracked and what things aren't?
function Counter() {
const [count, setCount] = createSignal(0);
createEffect(() => {
setTimeout(() => {
setCount(count() + 1);
}, 1000);
})
return (
<>
{count()}
</>
);
}
You can think about it this way (this is pretty much how the source code works):
let Listener
function Counter() {
const [count, setCount] = createSignal(0);
createEffect(() => {
Listener = thisEffect
setTimeout(() => {
setCount(count() + 1);
}, 1000);
Listener = null
})
return (
<>
{count()}
</>
);
}
As you can see the effect will set itself as the listener (tracking context) when the function starts and then will reset the listener (to the previous listener if it exists, in this case it doesn't).
So the effect will be the tracking context only during the execution of the callback you provided to createEffect as the argument. setTimeout delays the execution of whatever you put in it, so once the callback you put in setTimeout executes, the effect callback will have already finished executing, which means that it has already reset the listener, so the effect is not listening to signals anymore.
That is because the effect can not re-subscribe to the signal once it is run. Here is why:
Solid runs synchronously. Every signal keeps its own subscribers list. Effects are added to the subscribers list when they read the signal and removed when they are called back. So, subscribers list is renewed in each update cycle and it happens synchronously.
However setTimeout's callback is run asynchronously in the event loop.
When the callback runs, it will update the signal's value and the effect wrapping the setTimeout function will be added to the subscribers list. However this subscribers list gets discarded when the signal completes its execution cycle. So, the effect will never be called back. In other words, the effect will be subscribing to the subscribers list of the previous execution cycle.
The problem is not that the callback is unable to update the signal, (actually it does, that is why counter increments by one), but the effect is unable to re-subscribe to the signal's queue. So, we need to find a way to make the effect re-subscribe to the signal.
You have two options which produce different outputs:
Reads the signal synchronously which makes the effect re-subscribe and set a new timer whenever signal updates:
import { render } from "solid-js/web";
import { createSignal, createEffect } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
createEffect(() => {
const c = count();
setTimeout(() => {
setCount(c + 1);
}, 1000);
})
return (
<>
{count()}
</>
);
}
We read the signal's value in advance, long before the setTimeout's callback gets fired.
Get the right owner and subscribe to its list:
function Counter() {
const [count, setCount] = createSignal(0);
const owner = getOwner();
setTimeout(() => {
runWithOwner(owner!, () => {
createEffect(() => {
console.log('Running Effect');
setCount(count() + 1);
});
});
}, 1000);
return (
<>
{count()}
</>
);
}
In this solution, we create the effect when the setTimeout's callback is fired and bind the effect to the current owner.
An important side note: Your code will cause an infinite loop because you are setting the signal inside the effect, which runs whenever signal updates.
createEffect(() => {
setCount(count() + 1);
});
You can read more about runWithOwner function here: https://www.solidjs.com/docs/latest/api#runwithowner
It is easy enough to get the lat lng of a map click using something like:
map.on('click', function (e) {
coords= e.latlng.lat + ", " + e.latlng.lng;
});
But if there are shapes on the map the function doesn't get called if you click a place covered by a shape.
Ultimately I want to produce a popup window triggered when a shape is clicked and populated with information based on lat/long.
Welcome to SO!
You could bind your event listener on your shapes as well (possibly through an L.FeatureGroup to avoid having to bind to each individual shape), and you can even use that event listener to fire the "click" event on the map as well.
var shapes = L.featureGroup().addTo(map);
shapes.addLayer(/* some vector shape */); // As many times as individual shapes
shapes.on("click", function (event) {
shapecoords.innerHTML = event.latlng.toString();
map.fire("click", event); // Trigger a map click as well.
});
Demo: http://jsfiddle.net/ve2huzxw/40/
I'm trying to create new observable based on two others. I have:
var mouseClickObservable = Rx.Observable.fromEvent(this.canvas, "click");
var mouseMoveObservable = Rx.Observable.fromEvent(this.canvas, "mousemove");
function findObject(x, y) {/* logic for finding object under cursor here. */}
var objectUnderCursor = this.mouseMoveObservable.select(function (ev) {
return findObject(ev.clientX, clientY);
});
I want to create objectClicked observable, that should produce values when user clicks on an object. I could just call findObject again, like this:
var objectClicked = this.mouseClickObservable.select(function (ev) {
return findObject(ev.clientX, clientY);
});
but it's very time-consuming function.
Another way, which I currently use, is to store last hovered object in a variable, but I assume, there should be pure functional way of doing this. I tryed to use Observable.join like this:
var objectClicked = this.objectUnderCursor.join(
mouseClickObservable,
function (obj) { return this.objectUnderCursor },
function (ev) { return Rx.Observable.empty() },
function (obj, ev) { return obj })
but it produces multiple values for one click
I don't see any code where you actually subscribe to any of these observables you have defined, so it is hard to provide a good answer. Do you actually need to call findObject on every mouse move? Are you needing to provide some sort of hover effect as the mouse moves? Or do you just need to know the object that was clicked, in which case you only need to call findObject once when clicked?
Assuming you only need to know what object was clicked, you don't even worry about mouse moves and just do something like:
var objectClicked = mouseClickObservable
.select(function (ev) { return findObject(ev.clientX, ev.clientY); });
objectClicked.subscribe(function(o) { ... });
If you indeed need to know what object the mouse is hovering over, but want to avoid calling your expensive hit test also on a click, then indeed you need to store the intermediate value (which you are needing to store anyway to do your hovering effects). You can use a BehaviorSubject for this purpose:
this.objectUnderCursor = new Rx.BehaviorSubject();
mouseMoveObservable
.select(function (ev) { return findObject(ev.clientX, ev.clientY); })
.subscribe(this.objectUnderCursor);
this.objectUnderCursor.subscribe(function (o) { do your hover effects here });
mouseClickObservable
.selectMany(function () { return this.objectUnderCursor; })
.subscribe(function (o) { do your click effect });
I've seen the solution to drag and drop external events in fullcalendar. But, in this demo, all the external events have a duration of 2 hours (because defaultEventMinutes parameter is set to 120). I'm trying to change this demo in order to manage events with different durations. Say, "My event 1" is 45min long, "My event 2" is 165min, etc.
At the beginning I though there may be an attribute to store the duration in the eventObject, but according to the documentation, it's not the case.
Then, I thought it would be possible to change the value of 'defaultEventMinutes' when starting dragging the event. But apparently, I can't do it without rebuilding the whole calendar.
According to you, what is the best means to meet this requirement?
Thanks in advance for your advice...
Worked on this as well and have solved the duration shown on fullCalendar this way:
Having a custom "setOptions" function for fullCalendar.
Having a property for fullCalendar called "dragMinutes" that can be set during elements $(this).draggable({start:...}).
Here is the code for the custom setOptions:
...
function Calendar(element, options, eventSources) {
var t = this;
// hack for setting options that updates
function setOptions(new_options, refresh) {
$.extend(options, new_options);
if (refresh) {
var viewName = currentView.name;
changeView(viewName, true);
}
}
// exports ...
t.setOptions = setOptions;
...
Heres the code for handling "dragMinutes" option in fullCalendar:
/* External Dragging
--------------------------------------------------------------------------------*/
function dragStart(_dragElement, ev, ui) {
hoverListener.start(function (cell) {
clearOverlays();
if (cell) {
if (cellIsAllDay(cell)) {
renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
} else {
var d1 = cellDate(cell);
if (opt('dragMinutes'))
var d2 = addMinutes(cloneDate(d1), opt('dragMinutes'));
else
var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes'));
renderSlotOverlay(d1, d2);
}
}
}, ev);
}
And heres how i make event draggable and update the "dragMinutes":
// make the event draggable using jQuery UI
$(this).draggable({
containment: 'document',
// return a custom styled elemnt being dragged
helper: function (event) {
return $('<div class="uv-planning-dragging"></div>').html($(this).html());
},
opacity: 0.70,
zIndex: 10000,
appendTo: 'body',
cursor: 'move',
revertDuration: 0,
revert: true,
start: function (e, ui) {
// set the "dragMinutes" option in fullCalendar so shown interval about to be added is correct.
var data = $(this).data('eventObject');
if (data) {
var min = data.jsonProps.durationMsec / 1000 / 60;
if (macroCalendar.calendar) {
macroCalendar.calendar.fullCalendar('setOptions', { dragMinutes: Math.round(min) }, false);
}
}
},
stop: function (e, ui) {
// further process
}
});
Hope it helps.
If anyone still visits the thread and don't find the solution, the solution would be to set the duration parameter in event div... and then call draggable on that div.
$(this).data('event', {
title: 'new event title', // use the element's text as the event title
id: $(this).attr('id'),
stick: true, // maintain when user navigates (see docs on the renderEvent method)
duration: '03:00:00' // will set the duration during drag of event
});
Currently, the best solution I have found is adding a duration attribute on my event Object, then the code to create my fullCalendar looks like this:
$('#calendar').fullCalendar({
header: {
left: 'prev,next today',
center: 'title',
right: 'month,agendaWeek,agendaDay'
},
editable: true,
droppable: true, // this allows things to be dropped onto the calendar !!!
drop: function(date, allDay) { // this function is called when something is dropped
// retrieve the dropped element's stored Event Object
var originalEventObject = $(this).data('eventObject');
// we need to copy it, so that multiple events don't have a reference to the same object
var copiedEventObject = $.extend({}, originalEventObject);
// assign it the date that was reported
copiedEventObject.start = date;
// HERE I force the end date based on the start date + duration
copiedEventObject.end = new Date(date.getTime() + copiedEventObject.duration * 60 * 1000);
copiedEventObject.allDay = allDay;
// render the event on the calendar
// the last `true` argument determines if the event "sticks" (http://arshaw.com/fullcalendar/docs/event_rendering/renderEvent/)
$('#calendar').fullCalendar('renderEvent', copiedEventObject, true);
// is the "remove after drop" checkbox checked?
if ($('#drop-remove').is(':checked')) {
// if so, remove the element from the "Draggable Events" list
$(this).remove();
}
}
});
The only drawback is when you're dragging the event, the event duration looks like defaultEventMinutes and not the actual duration, but I don't know how to fix it
These special properties can either be specified in the provided event object, or they can be standalone data attributes:
<!-- DURATION OF 3 hours EVENT WILL PROPAGATE TO CALENDAR WHEN DROPPED -->
<div class='draggable' data-event='1' data-duration='03:00' />
https://fullcalendar.io/docs/dropping/eventReceive/
With the latest fullcalendar v2.0.2, if you want the overlay to be of the particular duration, you can update in this function of fullcalendar-arshaw.js
function dragStart(_dragElement, ev, ui) {
hoverListener.start(function(cell) {
clearOverlays();
if (cell) {
var seconds = duration_in_minutes * 1000 * 60 ;
// we need to pass seconds into milli-seconds
if (d1.hasTime()) {
d2.add(seconds);
renderSlotOverlay(d1, d2, cell.col);
}
else {
d2.add(calendar.defaultAllDayEventDuration);
renderDayOverlay(d1, d2, true, cell.col);
}
}
}, ev);
}
Here, pass your duration in the external events object and that object you can fetch in _dragElement and then convert it into milli-seconds and pass it in d2.add(seconds). This will create the shadow of that mili-seconds on that calendar.
For non-external events you can use the fullcalendar settings:
defaultTimedEventDuration: (hours+':00:00'),
forceEventDuration: true,
// defaultEventMinutes: hours*60, // not needed
and in the event data you do not set the end property (or you null it):
eventData = {
title: title,
start: start,
// end: end, // MUST HAVE no end for fixedduration
color: '#00AA00',
editable: true, // for dragging
};
Ref: http://fullcalendar.io/docs/event_data/defaultTimedEventDuration/
Tip: In case you want to prevent the resizing of the events which is possible due to editable: true, you can use CSS to hide the handle: .fc-resizer.fc-end-resizer { display:none; }
Since v4 some of the above options are not working at all. The problem i was facing was as follows:
All day items for me have a duration, but not a start time. When i select a start time by dragging, the start time is set but as soon as i set the end date ( which is done similar as above answers ), the end date is reset again.. there is something buggy going on in the setDate function... the end date is set, this part works, then it does a comparisson on itself to find out the time difference between the dates, but the date is already set by the system itself causing the difference to be 0 which is causing the enddate to be set to null again......
A giant pain in my neck i got to say... it works perfect when staying within the timeline, but that's about it.
I managed to 'fix', more like destroy it by using this line in the eventDrop event, but it will also work in any other events you may use:
update your event with ajax here, since you have the start and end date *
calendar.refetchEvents(); in the success function
This is going to refetch all the events, it sounds pretty killer for performance but it doesn't seem to take up much time, try it for yourself.
This way my titles, times etc are always up to date and the calendar is showing the right end date.
I have successfully implemented dragging of a jquery-ui element onto my fullCalendar. The problem is that what I want to drop onto is not the calendar itself but a specific event displayed on the calendar in order to add the dropped item to the event. The missing piece is how to identify the event that was under the mouse when I dropped.
drop: function (date, allDay, jsEvent, ui)
{
var event = ???;
event.description += ui.helper.data("filters").text;
$('#calendar').fullCalendar('updateEvent', event);
}
I've discovered the solution. Basically you have to add "droppable" to the event element. I do this by catching the "eventRender" (I assume this is a good spot)...
eventRender: function (event, element)
{
// store the ID for later...
$(element).data('id', event.id);
element.droppable({
drop: function (event, ui)
{
// get the ID I stored above...
var rowID = $(this).data('id');
I just implemented this - thankyou for your solution!
I'm using it in combination with drop - I need to be able to drop events either onto another event or onto a date.
In my case adding event.stopPropogation(); in element.droppable drop is necessary to stop the date drop function from also triggering.