haltu / muuri

Infinite responsive, sortable, filterable and draggable layouts
https://muuri.dev
MIT License
10.77k stars 644 forks source link

Patch suggestion: give more visibility to some internal stuff (angular usecase) #475

Open kkoo95 opened 3 years ago

kkoo95 commented 3 years ago

In order to make Muuri properly compatible with Angular I had wrap a lot of stuff to make sure that none of the positioning stuff would trigger Change Detection. Change detection is something that is automatically triggered in Angular (relying on ZoneJS) after such calls as setTimeout, requestAnimationFrame or event DOM listener callbacks. I used a Proxy to wrap all the public APIs of the various Muuri classes but some of them were not accessible at all from the outside world. I had to monkey patch muuri.module.js directly to change that. Basically I needed access to raf and ticker intances, and here are the following changes involved: I created an instance of each in the global Grid namespace (rather than in closure) and used that reference in respective places:

Index: third/muuri.module.js
===================================================================
@@ -1041,7 +1041,7 @@

 Ticker.prototype.add = function (laneIndex, id, callback) {
   this._lanes[laneIndex].add(id, callback);
-  if (!this._nextStep) this._nextStep = raf(this._step);
+  if (!this._nextStep) this._nextStep = Grid.raf(this._step);
 };

 Ticker.prototype.remove = function (laneIndex, id) {
@@ -1097,100 +1097,98 @@
 var LANE_READ_TAIL = 1;
 var LANE_WRITE = 2;

-var ticker = new Ticker(3);
-
 function addLayoutTick(itemId, read, write) {
-  ticker.add(LANE_READ, LAYOUT_READ + itemId, read);
-  ticker.add(LANE_WRITE, LAYOUT_WRITE + itemId, write);
+  Grid.ticker.add(LANE_READ, LAYOUT_READ + itemId, read);
+  Grid.ticker.add(LANE_WRITE, LAYOUT_WRITE + itemId, write);
 }

 function cancelLayoutTick(itemId) {
-  ticker.remove(LANE_READ, LAYOUT_READ + itemId);
-  ticker.remove(LANE_WRITE, LAYOUT_WRITE + itemId);
+  Grid.ticker.remove(LANE_READ, LAYOUT_READ + itemId);
+  Grid.ticker.remove(LANE_WRITE, LAYOUT_WRITE + itemId);
 }

 function addVisibilityTick(itemId, read, write) {
-  ticker.add(LANE_READ, VISIBILITY_READ + itemId, read);
-  ticker.add(LANE_WRITE, VISIBILITY_WRITE + itemId, write);
+  Grid.ticker.add(LANE_READ, VISIBILITY_READ + itemId, read);
+  Grid.ticker.add(LANE_WRITE, VISIBILITY_WRITE + itemId, write);
 }

 function cancelVisibilityTick(itemId) {
-  ticker.remove(LANE_READ, VISIBILITY_READ + itemId);
-  ticker.remove(LANE_WRITE, VISIBILITY_WRITE + itemId);
+  Grid.ticker.remove(LANE_READ, VISIBILITY_READ + itemId);
+  Grid.ticker.remove(LANE_WRITE, VISIBILITY_WRITE + itemId);
 }

 function addDragStartTick(itemId, read, write) {
-  ticker.add(LANE_READ, DRAG_START_READ + itemId, read);
-  ticker.add(LANE_WRITE, DRAG_START_WRITE + itemId, write);
+  Grid.ticker.add(LANE_READ, DRAG_START_READ + itemId, read);
+  Grid.ticker.add(LANE_WRITE, DRAG_START_WRITE + itemId, write);
 }

 function cancelDragStartTick(itemId) {
-  ticker.remove(LANE_READ, DRAG_START_READ + itemId);
-  ticker.remove(LANE_WRITE, DRAG_START_WRITE + itemId);
+  Grid.ticker.remove(LANE_READ, DRAG_START_READ + itemId);
+  Grid.ticker.remove(LANE_WRITE, DRAG_START_WRITE + itemId);
 }

 function addDragMoveTick(itemId, read, write) {
-  ticker.add(LANE_READ, DRAG_MOVE_READ + itemId, read);
-  ticker.add(LANE_WRITE, DRAG_MOVE_WRITE + itemId, write);
+  Grid.ticker.add(LANE_READ, DRAG_MOVE_READ + itemId, read);
+  Grid.ticker.add(LANE_WRITE, DRAG_MOVE_WRITE + itemId, write);
 }

 function cancelDragMoveTick(itemId) {
-  ticker.remove(LANE_READ, DRAG_MOVE_READ + itemId);
-  ticker.remove(LANE_WRITE, DRAG_MOVE_WRITE + itemId);
+  Grid.ticker.remove(LANE_READ, DRAG_MOVE_READ + itemId);
+  Grid.ticker.remove(LANE_WRITE, DRAG_MOVE_WRITE + itemId);
 }

 function addDragScrollTick(itemId, read, write) {
-  ticker.add(LANE_READ, DRAG_SCROLL_READ + itemId, read);
-  ticker.add(LANE_WRITE, DRAG_SCROLL_WRITE + itemId, write);
+  Grid.ticker.add(LANE_READ, DRAG_SCROLL_READ + itemId, read);
+  Grid.ticker.add(LANE_WRITE, DRAG_SCROLL_WRITE + itemId, write);
 }

 function cancelDragScrollTick(itemId) {
-  ticker.remove(LANE_READ, DRAG_SCROLL_READ + itemId);
-  ticker.remove(LANE_WRITE, DRAG_SCROLL_WRITE + itemId);
+  Grid.ticker.remove(LANE_READ, DRAG_SCROLL_READ + itemId);
+  Grid.ticker.remove(LANE_WRITE, DRAG_SCROLL_WRITE + itemId);
 }

 function addDragSortTick(itemId, read) {
-  ticker.add(LANE_READ_TAIL, DRAG_SORT_READ + itemId, read);
+  Grid.ticker.add(LANE_READ_TAIL, DRAG_SORT_READ + itemId, read);
 }

 function cancelDragSortTick(itemId) {
-  ticker.remove(LANE_READ_TAIL, DRAG_SORT_READ + itemId);
+  Grid.ticker.remove(LANE_READ_TAIL, DRAG_SORT_READ + itemId);
 }

 function addPlaceholderLayoutTick(itemId, read, write) {
-  ticker.add(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId, read);
-  ticker.add(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId, write);
+  Grid.ticker.add(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId, read);
+  Grid.ticker.add(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId, write);
 }

 function cancelPlaceholderLayoutTick(itemId) {
-  ticker.remove(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId);
-  ticker.remove(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId);
+  Grid.ticker.remove(LANE_READ, PLACEHOLDER_LAYOUT_READ + itemId);
+  Grid.ticker.remove(LANE_WRITE, PLACEHOLDER_LAYOUT_WRITE + itemId);
 }

 function addPlaceholderResizeTick(itemId, write) {
-  ticker.add(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId, write);
+  Grid.ticker.add(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId, write);
 }

 function cancelPlaceholderResizeTick(itemId) {
-  ticker.remove(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId);
+  Grid.ticker.remove(LANE_WRITE, PLACEHOLDER_RESIZE_WRITE + itemId);
 }

 function addAutoScrollTick(read, write) {
-  ticker.add(LANE_READ, AUTO_SCROLL_READ, read);
-  ticker.add(LANE_WRITE, AUTO_SCROLL_WRITE, write);
+  Grid.ticker.add(LANE_READ, AUTO_SCROLL_READ, read);
+  Grid.ticker.add(LANE_WRITE, AUTO_SCROLL_WRITE, write);
 }

 function cancelAutoScrollTick() {
-  ticker.remove(LANE_READ, AUTO_SCROLL_READ);
-  ticker.remove(LANE_WRITE, AUTO_SCROLL_WRITE);
+  Grid.ticker.remove(LANE_READ, AUTO_SCROLL_READ);
+  Grid.ticker.remove(LANE_WRITE, AUTO_SCROLL_WRITE);
 }

 function addDebounceTick(debounceId, read) {
-  ticker.add(LANE_READ, DEBOUNCE_READ + debounceId, read);
+  Grid.ticker.add(LANE_READ, DEBOUNCE_READ + debounceId, read);
 }

 function cancelDebounceTick(debounceId) {
-  ticker.remove(LANE_READ, DEBOUNCE_READ + debounceId);
+  Grid.ticker.remove(LANE_READ, DEBOUNCE_READ + debounceId);
 }

 var AXIS_X = 1;
@@ -7430,6 +7428,10 @@
  * *****************
  */

+Grid.raf = raf;
+
+Grid.ticker = new Ticker(3);
+
 /**
  * @public
  * @static

It would be cool if you intend to bring some of those changes, or something similar in a next version. Cheers

niklasramo commented 3 years ago

@kkoo95 Ugh, that sounds like loads of trouble to maintain indeed. Is there really no way to make Muuri compatible with Angular without touching the internal ticker system?

In any case, yep, I've been planning to allow user to provide a tick method option that would default to requestAnimationFrame, and would allow user to take control of the default requestAnimationFrame usage within Muuri. Also, as some other libs might use read and write queueing within requestAnimationFrame it might make sense to provide customization points for those too so that layout thrashing could be minimized in app level code.

kkoo95 commented 3 years ago

For angular i need to call those sensitive async methods "oustide angular zone", to avoid hundreds of change detection during DnD, scroll events and animations. I managed to proxify all public methods (as in !name.match('^(get)|_')) of Grid, ItemDrag and Autoscroller. I also had to do the same for Ticker and raf (as plain function proxy) so I made the changes i posted inspired by how autoscroller (or packer) is currently available to the outside world

ibrychgo commented 3 years ago

I'd like to second @kkoo95's comment.

Angular detects changes furiously. In my case, the application is based on firebase so there's another level of change detection happening. Given that Muuri is managing A LOT, it just complicates things way too much to handle all of that change detection without mucking something up.

The solution for me was to build all of the Muuri components in plain javascript, with listeners on the various controls and buttons. When muuri changes something (like moving an item from one grid to another via drag-and-drop) I rely on Muuri's various callbacks to take action on the database and update the local model itself. Yes, it removes the "shortcut" of having everything bound with angular, but it's way less code than playing puppeteer to all of that change detection. Our board has been been incredibly reliable without Angular sticking its head into everything.

On the database front, I simply have listeners on the nodes that contain grid data. One for changes: if a change is detected in a subgrid, I simply update my local model, destroy the original muuri grid and have muuri make a new one off of the new data. Surprisingly, this happens beautifully without flicker. If a "remove" is detected, I update my local model and then remove the item in Muuri. If an "add" is detected, I update my local model and then add the item with Muuri.

Keeping Muuri grids separate from the local model listeners and the database listeners was the best way I found to implement a rather complicated Muuri kanban board.