angular material drag&drop calls callback after css transition end - css

I have implemented list of elements which can be drag and dropped via angular material drag and drop feature
like in tutorial https://material.angular.io/cdk/drag-drop/overview
I've implemented function drop(event) however in my case i need not just move elements in angular model. I need to send request to server and when response from server will come update it. So my function is not changing anything, it creates request to server.
Problem here is that because angular model is not changed at drop function there is an element "jumping" occurs: after element drop element returns to the position where it was and when response from server comes and model is updated element move to new position. For user it's very painful to see that
I want to hide somehow from user that some work is going in the background.
Idea to have duplicated list which is for user and another one for server and update them (user list at the moment of drop and server list at the moment of response) I left as last decision because it's hard to maintain.
I'm trying to resolve it with css animations. As you can see in drag&drop example it uses
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
"transition: transform" to show smooth animation when user drops element. So i want to show user animation about 0.5s. This time should be enough for response from server and when animation is finished model is already updated.
What i'm seeing is that
(cdkDropListDropped)="drop($event)"
drop function is called AFTER transition is ended. So even if I do transform 2000ms drop function will be called after transition end, so after 2s. Is it expected? I thought function should be called after mouse release.
Do you have any thoughts for initial problem(hide model update) and for this css animation transition problem in particular?
Update:
I've created example of my problem
https://stackblitz.com/edit/angular-3nhsxx?file=src%2Fapp%2Fcdk-drag-drop-disabled-sorting-example.css,src%2Fapp%2Fcdk-drag-drop-disabled-sorting-example.ts

drop function is called AFTER transition is ended. So even if I do transform 2000ms drop function will be called after transition end, so after 2s. Is it expected? I thought function should be called after mouse release.
Yes, it is expected. The reason for this has a technically background. cdkDrag create a deep copy of the dragged element and set the position of the other items with position fixed. If the drag finished (mouse up) the animation starts and first when the animation is finished Angular will "resort" the items for a good transitions. Otherwise the elements will "jump" back to start order and then to new order.
You can use cdkDragReleased which fires directly before the animation ends. I think this is what you want. Stackblitz is updated.
Additional informations (container, index, item) can found in cdkDragExited and cdkDragEntered as example. More in the official docs.
Demo:
<div cdkDropListGroup>
<div class="example-container">
<h2>Available items</h2>
<div
cdkDropList
id="test"
[cdkDropListData]="items"
class="example-list"
cdkDropListSortingDisabled
(cdkDropListDropped)="drop($event)"
>
<div class="example-box" *ngFor="let item of items" (cdkDragEntered)="cdkDropListEntered($event)"
(cdkDragExited)="cdkDropListExited($event)" (cdkDragReleased)="dragEnd($event)" cdkDrag>{{item}}</div>
</div>
</div>
<div class="example-container">
<h2>Shopping basket</h2>
<div
id="super"
cdkDropList
[cdkDropListData]="basket"
class="example-list"
(cdkDropListDropped)="drop($event)"
>
<div class="example-box" *ngFor="let item of basket" (cdkDragEntered)="cdkDropListEntered($event)"
(cdkDragExited)="cdkDropListExited($event)" (cdkDragReleased)="dragEnd($event)" cdkDrag>{{item}}</div>
</div>
</div>
</div>
Code
dragEnd(event: any) {
console.log('End', event);
}
cdkDropListEntered(event: any) {
console.log(event);
}
cdkDropListExited(event: any) {
console.log(event);
}

Related

Headless UI Transition.child errors to "Did you forget to passthrough the `ref` to the actual DOM node"

I'm building a sidebar with the Transition and Dialog Headless UI components.
Transition docs
When I break out the code that's passed between <Transition.Child> to it's own component. I get this error:
Unhandled Runtime Error
Error: Did you forget to passthrough the `ref` to the actual DOM node?
Call Stack
eval
node_modules/#headlessui/react/dist/components/transitions/transition.js (1:3632)
Failing code:
<Transition.Child as={Fragment}>
<Cart
cancelButtonReference={cancelButtonReference}
setCartOpen={setCartOpen}
checkoutUrl={checkoutUrl}
removeCartItem={removeCartItem}
clearCart={clearCart}
cartLoading={cartLoading}
incrementCartItem={incrementCartItem}
decrementCartItem={decrementCartItem}
cartTotal={cartTotal}
cart={cart}
/>
</Transition.Child>
Working code:
<Transition.Child as={Fragment}>
<div>
...
</div>
</Transition.Child>
I understand the error I believe, which is that when I break out the code to it's own component Transition.Child wants me to pass a ref so that React knows that it should render a component and not a fragment.
If I remove as={Fragment}, which makes the Transition default to a div the error goes away, but then I get an unneeded div..
What ref do I need to pass? This is what I don't get.
You don't need to pass a ref, but the component needs to accept one and set it on the actual element.
The div element will accept the ref, which is why that method works.
Try creating the Cart component using React.forwardRef and set the ref on the div.
const Cart = React.forwardRef((props, forwardedRef) => {
return (
<div ref={forwardedRef}>
...
</div>
)
})
I ran into the same issue, and just found a solution—which you basically already said, too.
Through some testing, as you pointed out, it works when surrounding the component with the <div>…</div> tags.
Although I don't really understand it, it's clear that Headless UI's <Transition> tag wants to pass a reference to it's immediate child. With normal HTML tags, like <div>, it does it automatically. When using a component, it can't do it in the same way.
Solution #1
(Broken*)
I'm sure there's a more "proper" solution to this, but I found that—as we don't want <Transition> to render any HTML tags—you can just surround your component with another React fragment:
<Transition.Child as={Fragment}>
<> {/* <— Our new Fragment */}
<Cart
cancelButtonReference={cancelButtonReference}
setCartOpen={setCartOpen}
checkoutUrl={checkoutUrl}
removeCartItem={removeCartItem}
clearCart={clearCart}
cartLoading={cartLoading}
incrementCartItem={incrementCartItem}
decrementCartItem={decrementCartItem}
cartTotal={cartTotal}
cart={cart}
/>
</>
</Transition.Child>
This was working for me for a bit, but then just stopped working.*
Solution #2
(This also appears to not function correctly*)
As a secondary option, if you don't mind having another element rendered on the DOM, you can set the <Transition> to render as={'div'} (this is the default, so you don't actually have to define the prop), and then set the CSS display to contents:
<Transition.Child style={{display: 'contents'}}> {/* <— display set to contents */}
<Cart
cancelButtonReference={cancelButtonReference}
setCartOpen={setCartOpen}
checkoutUrl={checkoutUrl}
removeCartItem={removeCartItem}
clearCart={clearCart}
cartLoading={cartLoading}
incrementCartItem={incrementCartItem}
decrementCartItem={decrementCartItem}
cartTotal={cartTotal}
cart={cart}
/>
</Transition.Child>
If you're not familiar with display: contents, Manuel Rego give this description:
display: contents makes that the div doesn’t generate any box, so its background, border and padding are not rendered. However the inherited properties like color and font have effect on the child (span element) as expected.
Source
It seems that this technically fixes the ref issue, but because of the way display: contents works, all other applied CSS doesn't render.*
As I just discovered, neither of these works, I am looking into other solutions.

Vue/Vuetify transition only one way

Is it possible to set transition to trigger only one way for the v-btn?
I created button with text that changes depending on the condition and use transition to change it smoothly. However if i change view and condition changes the transition reverses and it looks glitchy. I dont want this behaviour, and only would like to have transition trigger one way.
<v-btn
class="ma-1"
#click="addToList"
width
:disabled="isAdded"
>
<span v-if="!isAdded">
Add to List
</span>
<v-slide-x-transition>
<span v-if="isAdded">
Added
<v-icon>
mdi-check
</v-icon>
</span>
</v-slide-x-transition>
</v-btn>
Here is example how this works:
https://codepen.io/pokepim/pen/VwKNzxj?editors=101
In the carousel when you switch between slides you can see that button reverses back and it extends width and generally this looks somewhat glitchy. Is it possible to only keep transition when clicking on the button in this case (so no backward transition from disable to active button)?
EDIT:
I tried Dan's answer, and while it works fine with the carousel case it still does not solve glitchy transition in general, here is the example of same behaviour using method instead of computed property:
https://codesandbox.io/s/vuetifyvuex-store-testing-ground-wyl4n?file=/src/App.vue
There's only one isAdded condition being used for all of the slides. When that condition is false on the next slide, it gets set before the animation is finished and it looks bad.
Use a method instead of a computed for isAdded so each slide can maintain its own button state:
methods: {
addToList() {
this.btnlist.push(this.model);
},
isAdded(i) {
return this.btnlist.includes(i);
}
}
And change all isAdded in the template to isAdded(i).
Here's the updated demo
To your edit, try the hide-on-leave attribute on the transition:
<v-slide-x-transition hide-on-leave>
<span v-if="isAdded(this.idUn)">
Added
<v-icon> mdi-check </v-icon>
</span>
</v-slide-x-transition>

Is it possible to position a div on top of a <dialog> tag that is not its parent?

The <dialog> tag, when opened with showModal(), will display the elements between it and its closing tag while disabling all other elements on the page. My question is: is it possible to override this behavior for a specific element? Example:
HTML:
<div id="container">
<dialog id="myDialog">
<button id="close" type="reset">Close</button>
<button id="create">Add Element</button>
</dialog>
</div>
<menu>
<button id="openButton">Open Dialog</button>
</menu>
CSS:
.new-element {
width: 50px;
height: 50px;
border: 3px solid black;
background-color: blue;
position: fixed;
top: 50%;
}
JS:
const container = document.getElementById('container');
const openButton = document.getElementById('openButton');
const closeButton = document.getElementById('close');
const createButton = document.getElementById('create');
const myDialog = document.getElementById('myDialog');
openButton.addEventListener('click', function() {
myDialog.showModal();
});
closeButton.addEventListener('click', function() {
myDialog.close();
});
createButton.addEventListener('click', function() {
const div = document.createElement('div');
div.classList.add('new-element')
container.appendChild(div);
});
In a JSFiddle: https://jsfiddle.net/y7bkxvd4/
I'd like to find a way to position the blue square on top of the dialog. I realize it would be far easier to just append the new div to the dialog itself, but I've run into this in a situation where overflow was a concern and in using a module that uses react-portal. If it's not possible, cool, I can get behind that. But if it is, I'd like to know.
z-index has no effect, obviously.
The <dialog> element is added to the 'Top Layer' of the dom which has its own stacking order (z-index does not affect this -- it is set strictly by the order by which the elements are added). I don't believe you can manually add elements to this Top Layer, it is done in functions such as showModal(). You can find more information at: https://fullscreen.spec.whatwg.org/#new-stacking-layer but because the feature is still not universally supported its tough finding documentation on it. For example:
To remove an element from a top layer, remove element from top layer.
Real helpful..
A couple work arounds:
Change the added element to a dialog as well and call .showModal() when the element is appended. The problem with this approach is that .showModal() makes all element outside that element unavailable for user interaction. That means that your blue box is on top, but its also means you can't click "Close" or "Add Element" on the other modal. (NOTE: You'll also notice the "Close/Add Element" dialog is greyed out -- you can override this by changing .new-element::backdrop{...} but it still won't the change the fact you can't click "Close" or "Add Element") Example here (with the backdrop removed)
Change the added element to dialog, call .show() when the element is appended, and change the click event for 'Open Dialog' to .show() instead of .showModal. This allows you to also click past the blue box (even though its 'on top'), but it also allows you to click anywhere on the page (kind of defeating the purpose of a modal). The ::backdrop pseudo element is also not available because you are not using .showModal If you take this approach you would need to attach closing the blue box to the "Close" click event handler. Example here
My recommendation is to either use a plugin for modals (such as Bootstrap's) or make your own with the functionality you want (using Javascript). Dialogs are technically experiential technology so it won't be easy trying to get the behavior you want out of the box. This is probably as close as you will get, though you could improve it by adding your own "backdrop".

Work with complex animations on Blaze

I actually looking for a good way to play animations on my app context with Blaze.
To be more explicit, I wrote this super simple example:
<template name="global">
<h1>Hi guys!</h1>
{{> foo}}
</template>
<template name="foo">
<h2>I'm a foo!</h2>
<ul>
{{#each elements}}
{{> bar}}
}}
</ul>
<button name="btnAdd">Add new elem</button>
<button name="btnDel">Delete an elem</button>
</template>
<template name="bar">
<li>{{name}}</li>
</template>
Let's assume we got an Iron-router route which render the global Template.
On this particular render (from "navigate") I want each templates to render with fadeIn.
When click on btnAdd button, a new element created. I wish it would render with SlideInLeft effect.
When click on btnDel button, an element is deleted. I wish it would be destroyed with SlideOutRight effect.
When user navigate to another route, I want all template disappear with fadeOut effect.
Every of my attempt so far wouldn't allow me to do this kind of distinction... I couldn't find any package resolving this problem neither.
I'm actually just playing animation by adding/removing Animate.css class (pretty simple to use and good looking!)
To resume, I want a different animation played depending on the source of the rendering.
Does someone had already face this problem?
BONUS QUESTION: Do you know how to chain animations, like:
render global with fadeIn Effect >> then >> render foo with rotateIn Effect >> then >> render every bar with bounceIn effect
For timing, you can use Meteor.setInterval. For example, you can do $('.elementClass').hide('fast') outside setInterval. It will run first and setInterval will run when you want it to.
For the initial effects, you can use:
Template.templateName.onRendered(function(){
$('.elementClass').fadeIn('fast') //note that element is initially hidden (display:none in CSS). you can use effects from jquery and jquery-ui for more effects. You have to add jquery-ui additionally
})
You can also use jQuery in your router.js, using iron:router.
Router.route('/the-url', function() {
this.render('templateName', {
data: function () {
$('.htmlElement').toggle('slide', {direction:'right'}, 200); //note that the element is initially invisible
}
});
}, {
name: 'routeName'
});

Ember.js binding to component/view style attributes

I'm trying to create a component, or a view if that is better suited for this, that binds to given model's computed properties and changes style attributes accordingly. I'm using Ember App Kit if that affects in anyway how this should be done.
The component would be a a meter that has 2 computed properties to bind to: "limitDeg", "currentValueDeg". Every model instance that will use this component can supply those as model computed properties to the component.
The meter has 3 overlapping divs - ".meter-div" being just background with no bindings, and one of which will be rotated with CSS3 transform by X-degrees to show the current "limitDeg". So adjusting the "transform: rotate(###deg);" is the key here. There is a "indicator-div", that is simple indicator that similarly rotates based on the "currentValueDeg" with CSS3 animation loop.
Below is a basic rough outline of how I've thought of having the component/view:
<div class="my-component-container">
<div class="limit-div"></div>
<div class="meter-div"></div>
<div class="indicator-div"></div>
</div>
...and I would use it like this for example:
{{#each}}
...
{{my-component}}
...
{{/each}}
1) I would like the component to bind to the model so, that when the "limitDeg" computed property changes, the ".limit-div" will be rotated by X-degrees with CSS3 transform: rotate(###degrees);.
2) I would like animate the ".indicator-div" with a CSS3 animation that loops infinitely, binding to the currentValueDeg computedProperty.
Is this something I should even try do this with one component/view, or rather do it this multiple components/views inside one partial?
If you're using handlebars, just wrap your component in tags that specify it as such:
<script type='text/x-handlebars' data-template-name='components/component_name'>
<div class="limit-div"></div>
<div class="meter-div"></div>
<div class="indicator-div"></div>
</script>
Then include it in your view like this:
{{component_name objectToPassIn=this classNames='my-component-container' tagName='div'}}
If you wanted the component to display a model property, you could do something like this inside the component (using the variables in my example above):
<span class='property-wrapper'>
{{objectToPassIn.objectProperty}}
</span>
Edit:
For the sake of clarity, objectToPassIn is the model that is passed to the view that calls the component.

Resources