How to communicate an action to a React component - reactjs

While my scenario is pretty specific, I think it speaks to a bigger question in Flux. Components should be simple renderings of data from a store, but what if your component renders a third-party component which is stateful? How does one interact with this third-party component while still obeying the rules of Flux?
So, I have a React app that contains a video player (using clappr). A good example is seeking. When I click a location on the progress bar, I want to seek the video player. Here is what I have right now (using RefluxJS). I've tried to strip down my code to the most relevant parts.
var PlayerActions = Reflux.createActions([
'seek'
]);
var PlayerStore = Reflux.createStore({
listenables: [
PlayerActions
],
onSeek(seekTo) {
this.data.seekTo = seekTo;
this.trigger(this.data);
}
});
var Player = React.createClass({
mixins: [Reflux.listenTo(PlayerStore, 'onStoreChange')],
onStoreChange(data) {
if (data.seekTo !== this.state.seekTo) {
window.player.seek(data.seekTo);
}
// apply state
this.setState(data);
}
componentDidMount() {
// build a player
window.player = new Clappr.Player({
source: this.props.sourcePath,
chromeless: true,
useDvrControls: true,
parentId: '#player',
width: this.props.width
});
},
componentWillUnmount() {
window.player.destroy();
window.player = null;
},
shouldComponentUpdate() {
// if React realized we were manipulating DOM, it'd certainly freak out
return false;
},
render() {
return <div id='player'/>;
}
});
The bug I have with this code is when you try to seek to the same place twice. Imagine the video player continuously playing. Click on the progress bar to seek. Don't move the mouse, and wait a few seconds. Click on the progress bar again on the same place as before. The value of data.seekTo did not change, so window.player.seek is not called the second time.
I've considered a few possibilities to solve this, but I'm not sure which is more correct. Input requested...
1: Reset seekTo after it is used
Simply resetting seekTo seems like the simplest solution, though it's certainly no more elegant. Ultimately, this feels more like a band-aid.
This would be as simple as ...
window.player.on('player_seek', PlayerActions.resetSeek);
2: Create a separate store that acts more like a pass-through
Basically, I would listen to a SeekStore, but in reality, this would act as a pass-through, making it more like an action that a store. This solution feels like a hack of Flux, but I think it would work.
var PlayerActions = Reflux.createActions([
'seek'
]);
var SeekStore = Reflux.createStore({
listenables: [
PlayerActions
],
onSeek(seekTo) {
this.trigger(seekTo);
}
});
var Player = React.createClass({
mixins: [Reflux.listenTo(SeekStore, 'onStoreChange')],
onStoreChange(seekTo) {
window.player.seek(seekTo);
}
});
3: Interact with window.player within my actions
When I think about it, this feels correct, since calling window.player.seek is in fact an action. The only weird bit is that I don't feel right interacting with window inside the actions. Maybe that's just an irrational thought, though.
var PlayerActions = Reflux.createActions({
seek: {asyncResult: true}
});
PlayerActions.seek.listen(seekTo => {
if (window.player) {
try {
window.player.seek(seekTo);
PlayerActions.seek.completed(err);
} catch (err) {
PlayerActions.seek.failed(err);
}
} else {
PlayerActions.seek.failed(new Error('player not initialized'));
}
});
BTW, there's a whole other elephant in the room that I didn't touch on. In all of my examples, the player is stored as window.player. Clappr did this automatically in older versions, but though it has since been fixed to work with Browserify, we continue to store it on the window (tech debt). Obviously, my third solution is leveraging that fact, which it technically a bad thing to be doing. Anyway, before anyone points that out, understood and noted.
4: Seek via dispatchEvent
I also understand that dispatching a custom event would get the job done, but this feels way wrong considering I have Flux in place. This feels like I'm going outside of my Flux architecture to get the job done. I should be able to do it and stay inside the Flux playground.
var PlayerActions = Reflux.createActions({
seek: {asyncResult: true}
});
PlayerActions.seek.listen(seekTo => {
try {
let event = new window.CustomEvent('seekEvent', {detail: seekTo});
window.dispatchEvent(event);
PlayerActions.seek.completed(err);
} catch (err) {
PlayerActions.seek.failed(err);
}
});
var Player = React.createClass({
componentDidMount() {
window.addEventListener('seekEvent', this.onSeek);
},
componentWillUnmount() {
window.removeEventListener('seekEvent', this.onSeek);
},
onSeek(e) {
window.player.seek(e.detail);
}
});

5: keep the playing position in state (as noted by Dan Kaufman)
Could be done something like this:
handlePlay () {
this._interval = setInterval(() => this.setState({curPos: this.state.curPos + 1}), 1000)
this.setState({playing: true}) // might not be needed
}
handlePauserOrStop () {
clearInterval(this._interval)
this.setState({playing: false})
}
componentWillUnmount () {
clearInteral(this._interval)
}
onStoreChange (data) {
const diff = Math.abs(data.seekTo - this.state.curPos)
if (diff > 2) { // adjust 2 to your unit of time
window.player.seek(data.seekTo);
}
}

Related

Intelligent use of shouldComponentUpdate to help browser stay responsive

I have a realtime application that gets lots of data. As a result, during lot of handing of the data, the browser becomes unresponsive. I am researching using Promises, or Rx, but those seem to involve more surgery than I want to commit to at this point.
I read that shouldComponentUpdate can be used for this purpose. Would say using it to update once a second be possible [smart is probably what I am looking for] and what would the code look like inside shouldComponentUpdate:
var HomePage = React.createClass({
getInitialState: function() {
var stocks = {};
feed.onChange(function(stock) {
stocks[stock.symbol] = stock;
this.setState({stocks: stocks, bid: stock, ask: stock, last: stock, type: stock, undsymbol: stock});
}.bind(this));
return {
stocks: stocks
};
},
componentDidMount() {
var props = this.props;
},
shouldComponentUpdate(nextProps, nextState) {
// What code would go in here to render
// all changes, say every 1 second only?
},
etc...
}
Possible? Yes. Best practice? Probably not.
shouldComponentUpdate should return a boolean: true if a new update/render cycle is to be run, false if not.
one way to throttle rendering using shouldComponentUpdate is to do timestamp comparisons:
shouldComponentUpdate() {
const now = Date.now();
if (!this.lastUpdated || now - this.lastUpdated > 1000) {
this.lastUpdated = now;
return true;
}
return false;
}

how to make a relations between angular 1 components?

i'm looking for some solution to work with angular 1.5 components in a way that a change in one component will change something in other component in another place in the tree.
what so you think is the best solution?
rootScope?
redux?
events?
some other global variable?
If you want your own service without redux or anything else, you can implement a service like :
angular.module("my_service", [])
.factory("Message", function() {
var messages = {
// m1: [],
// m2: []
};
function receive(message, messageHandler) {
if (!Array.isArray(messages[message])) {
messages[message] = [];
}
messages[message].push(messageHandler);
}
function send(messageName, message) {
if (Array.isArray(messages[messageName])) {
messages[messageName].forEach(function(messageHandler) {
messageHandler(message);
});
} else {
console.warn("sent message", message, "is not in the message list...");
}
}
return {
send: send,
receive: receive
};
}
});
And in somewhere else you can create two controllers for example :
angular.module("app", ["my_service"])
.controller("app1", function(Message) {
Message.receive("sthHappened", function(whatHappened) {
console.log("app1 says :", whatHappened);
});
})
.controller("app2", function(Message) {
Message.send("sthHappened", "app2 initiated");
});
Basically you register a function in order to execute when a message received, and a trigger when something happened in order to execute the registered functions.
Of course you may need to make some additions according to what you need and improve performance or async op in order to break the sequential execution, but this the basic structure of a publisher-subscriber mechanism in order to create a messaging channel between your controllers or directives or any functions.

Angular-Leaflet heatmap data update

I'm using the Angular-Leaflet directive to display a heatmap, and I want the data to evolve through time. My code looks like:
getData().then(function (data) {
$scope.heat.index = 0;
$scope.heat.data = data;
$scope.layers.overlays.heat = {
name: "Heatmap",
type: "heat",
data: $scope.heat.data[$scope.heat.index], // data is a dictionary, not an array
visible: true
};
$scope.$watch('heat.index', function (new_index) {
$scope.layers.overlays.heat.data = $scope.heat.data[new_index];
});
});
However, when I change data.index (through a slider), nothing happens. What could be going on? I know that Angular-Leaflet supports this behavior because of this Github issue where someone added it.
leafletData.getMap().then(function (map) {
map.eachLayer(function (layer) {
if (layer.options.layerName == 'heatmap') {
layer.setLatLngs(newHeatmapData);
}
})
})
This worked for me.
Leaflet.heat provides a redraw() method but that didn't work for me.

Capturing click events on clusters with markercluster and angular-leaflet-directive

I'm playing with the angular-leaflet-directive, and getting the marker names from a mouse click is straight forward. I just listen for the leafletDirectiveMarker.click event and then access args.markerName.
angular-leaflet-directive also works with markercluster, so I can cluster markers that have the same coordinates or ones that are close by. However, I would like to do the following, but it is not clear from the documentation on how to do it:
Make user double-click on cluster to zoom in. Currently doing a single click on a cluster will zoom in on the markers. see example.
How to listen for click event on cluster and get all marker names in the cluster.
The documentation for clustermarker has a cluster event:
markers.on('clusterclick', function (a) {
console.log('cluster ' + a.layer.getAllChildMarkers().length);
});
But I'm not sure what event I should be listening to using angular-leaflet-directive.
As far as your first question goes, you'll have to hook the doubleclick and pass it the fire('click') command after overriding the usual click event. Probably more trouble than its really worth, especially on mobile - and not something I can easily solve.
Regarding your second question, I have just solved it.
$scope.openMarker is a reference to an ng-click event in my jade template that is attached to an ng-repeat which pulls images and their id's from the database.
$scope.openMarker = function(id) {
var _this = [];
_this.id = id;
leafletData.getMarkers()
.then(function(markers) {
$scope.london = {
lat: $scope.markers[_this.id].lat,
lng: $scope.markers[_this.id].lng,
zoom: 19
};
var _markers = [];
_markers.currentMarker = markers[_this.id];
_markers.currentParent = _markers.currentMarker.__parent._group;
_markers.visibleParent = _markers.currentParent.getVisibleParent(markers[id]);
_markers.markers = markers;
return _markers;
}).then(function(_markers){
if (_markers.visibleParent !== null) {
_markers.visibleParent.fire('clusterclick');
} else {
_markers.currentMarker.fire('click');
}
return _markers;
}).then(function(_markers){
_markers.currentParent.zoomToShowLayer(_markers.markers[ _this.id ], function() {
$scope.hamburg = {
lat: $scope.markers[_this.id].lat,
lng: $scope.markers[_this.id].lng,
zoom: 19
};
if (_markers.currentMarker !== null) {
_markers.currentMarker.fire('click');
} else {
_markers.visibleParent.fire('clusterclick');
_markers.currentMarker.fire('click');
}
});
});
};
You can read more about how I came to this solution here at github.
Much like many people, I too had a long search with no results. While experimenting with another method, I came across this:
$timeout(function(){
leafletData.getLayers().then(function(layers) {
$scope.markerClusterGrp = layers.overlays.locations;
var clusters = $scope.markerClusterGrp.getLayers();
$scope.markerClusterGrp.on('clustermouseover', function (a) {
var clusterObjects = a.layer.getAllChildMarkers();
console.log(clusterObjects);
});
$scope.markerClusterGrp.on('clusterclick', function (a) {
var clusterObjects = a.layer.getAllChildMarkers();
console.log(clusterObjects);
});
});
},1000);
It works the same, the difference is that it requires a timeout in order to wait for the layer to render with all markers (my understanding, correct me if wrong :-) ).
I hope this helps anyone searching for an angular solution. Just remember to include $timeout in your controller dependencies.

Drupal.attachBehaviours with jQuery infinitescroll and jQuery masonry

I am a little desperate here. I have been reading everything I was able to find on Drupal.behaviours but obviously its still not enough. I try running a masonry grid with the infinitescroll plugin to attach the new images to the masonry. This works fine so far. The next thing I wanted to implement to my website is a hover effect (which shows information on the images) and later fancybox to show the images in a huger size.
(function ($) {
Drupal.behaviors.views_fluidgrid = {
attach: function (context) {
$('.views-fluidgrid-wrapper:not(.views-fluidgrid-processed)', context).addClass('views-fluidgrid-processed').each(function () {
// hide items while loading
var $this = $(this).css({opacity: 0}),
id = $(this).attr('id'),
settings = Drupal.settings.viewsFluidGrid[id];
$this.imagesLoaded(function() {
// show items after .imagesLoaded()
$this.animate({opacity: 1});
$this.masonry({
//the masonry settings
});
});
//implement the function of jquery.infinitescroll.min.js
$this.infinitescroll({
//the infinitescroll settings
},
//show new items and attach behaviours in callback
function(newElems) {
var newItems = $(newElems).css({opacity: 0});
$(newItems).imagesLoaded(function() {
$(newItems).animate({opacity: 1});
$this.masonry('appended', newItems);
Drupal.attachBehaviours(newItems);
});
});
});
}
};
})(jQuery);
Now I read that I need to Reattach the Drupal.behaviours if I want the hover event to also take place on the newly added content.
(function ($) {
Drupal.behaviors.imgOverlay = {
attach: function (context) {
var timeout;
$('.img_gallery').hover(function() {
$this = $(this);
timeout = setTimeout(change_opacity, 500);
}, reset_opacity);
function change_opacity() {
//set opacity to show the desired elements
}
function reset_opacity() {
clearTimeout(timeout);
//reset opacity to 0 on desired elements
}
}
};
})(jQuery)
Where do I now write the Drupal.attachBehaviours() to make it work actually? Or is there some other error I just dont see atm? I hope I wrote the question so that its understandable and maybe it also helps somebody else, since I experienced that there is no real "official" running Version of this combination in drupal 7.
Ok, the solution is actually pretty simple. When writing it correctly than it also runs. its of course not Drupal.attachBehaviours() but Drupal.attachBehaviors() . So this combination now works and I am finally relieved :).

Resources