Gauge.js Component under Vue.js 3.2.1 not Creating - vuejs2

"Hi, I'm trying to get a simple gauge.js to work under Vue.js v3.2.1. The document reference get is always returning a null so that the mounted function never completes. This should be a simple thing, but I fear I am missing knowledge somewhere as I am new to Vue.js & can't find anything to help. "
<template >
<canvas ref="foo"></canvas>
</template>
<script>
import { Gauge } from 'gauge.js'
export default
{
name: 'vcGaugeJs',
props:
{
value: {type: Number, default: 0},
//options: { type: Object, default: () => ({}) }
},
data()
{
return
{
gauge: null
}
},
mounted: function ()
{
this.initGauge();
},
watch:
{
value: function (val)
{
this.gauge.set(val);
},
},
updated: function()
{
if (this.gauge == null)
{
this.initGauge();
}
},
methods:
{
initGauge ()
{
let opts =
{
angle: 0, // The span of the gauge arc
lineWidth: 0.35, // The line thickness
radiusScale: 1, // Relative radius
pointer:
{
length: 0.53, // // Relative to gauge radius
strokeWidth: 0.057, // The thickness
color: '#000000' // Fill color
},
limitMax: false, // If false, max value increases automatically if value > maxValue
limitMin: false, // If true, the min value of the gauge will be fixed
colorStart: '#6F6EA0', // Colors
colorStop: '#C0C0DB', // just experiment with them
strokeColor: '#EEEEEE', // to see which ones work best for you
generateGradient: true,
highDpiSupport: true // High resolution support
}
var target = this.$refs.foo;
if (target == null)
{
console.log ("Null target ref!");
}
else
{
this.gauge = new Gauge(this.$refs.foo);
this.gauge.maxValue = 3000; // set max gauge value
this.gauge.setMinValue(0); // Prefer setter over gauge.minValue = 0
this.gauge.animationSpeed = 62; // set animation speed (32 is default value)
this.gauge.set(1700); // set actual value
this.gauge.setOptions(opts); // create gauge!
}
}
},
}

Here's what the dev team says:
If you look at the lifecycle digram you can see that when the created() hook is called, the component’s template/render function has not been compiled.
So in your case, you should be able to instantiate the gauge at mounted hook on vm.$el instead, with it being the canvas element.
mounted() {
this.initGauge();
},
methods: {
initGauge() {
let opts = { /* options */}
this.gauge = new Gauge(this.$el).setOptions(opts);
this.gauge.maxValue = 3000; // set max gauge value
this.gauge.setMinValue(0); // Prefer setter over gauge.minValue = 0
this.gauge.animationSpeed = 62; // set animation speed (32 is default value)
this.gauge.set(1700); // set actual value
}
}

Related

Vue.js UI does not update the state on window reload

I have the following code structure (simplified):
<template v-for="tile of layer.data">
<VueDragResize :x="calculatePositionX(tile)">
<h2 :style="{'font-size':calculateFontSize(tile) + 'px'}">Test</h2>
</VueDragResize>
</template>
The :x="calculatePositionX(tile) and :style="{'font-size':calculateFontSize(tile) + 'px'}" values are dynamic. I want to change these values on window resize.
To achieve this I do this: (too keep it simple I want to show you only the calculatePositionX example).
mounted() {
window.addEventListener('resize', this.onResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.onResize)
},
methods: {
onResize(event) {
this.calculatePositionX(null);
},
calculatePositionX(tile) {
if (!tile) {
tile = this.cachedTile;
}
this.cachedTile = tile;
let x = tile.boxPositionX;
let parentWidth = tile.boxPositionParentWidth;
let currentParentWidth = this.$refs.builderLayer.clientWidth;
if (parentWidth == currentParentWidth) {
return x;
}
console.log(currentParentWidth * x / parentWidth); // Returns different result as the initial, but the state is not updating
return currentParentWidth * x / parentWidth;
},
}
data() {
return {
cachedTile: Object,
}
},
The console.log(currentParentWidth * x / parentWidth); returns a different result as the initial, but the state is not updating on the UI.
What can be the issue here? I also tried to save the new value as variable into the store and return the store result. Without success.
What do you think?
You defined calculatePositionX on the Vue instance itself.
Move it inside methods and it will work.
Side note: you might want to replace cachedTile: Object with cachedTile: {} in data.
In the OP code, you call calculatePositionX on resize, but ignore the return. The template won't know to update on the resize, but it will if dependent data changes.
Rather than returning the result of the resize calculation, have your calculatePositionX method change the state, then, let vue reactive getters do their job in the template. Refer to x in data (which is set by the slightly modified calculatePositionX)
<template v-for="tile of layer.data">
<VueDragResize :x="x">
<h2 :style="style(tile)">Test</h2>
</VueDragResize>
</template>
mounted() {
window.addEventListener('resize', this.onResize);
this.onResize();
},
beforeDestroy() {
window.removeEventListener('resize', this.onResize)
},
methods: {
onResize(event) {
this.calculatePositionX(null);
},
calculatePositionX(tile) {
if (!tile) {
tile = this.cachedTile;
}
this.cachedTile = tile;
let x = tile.boxPositionX;
let parentWidth = tile.boxPositionParentWidth;
let currentParentWidth = this.$refs.builderLayer.clientWidth;
if (parentWidth == currentParentWidth) {
this.x = x;
return;
}
console.log(currentParentWidth * x / parentWidth);
this.x = currentParentWidth * x / parentWidth;
},
style(tile) {
return {'font-size':calculateFontSize(tile) + 'px'}
}
}
data() {
return {
x: 0, // will get initialized in mounted
cachedTile: Object,
}
},

ThreeJS component working in VueJS 2 but not 3

I'm upgrading my app to VueJS 3. I read that you could keep the same components. But I have an error in the console now, although I didn't change anything. Here is my component:
<template>
<v-container>
<div
#click="onClick"
#mousemove="onMouseMove"
id="menu3D"
style="background-color: transparent; position: fixed; left: 20px; width:15%; height:100%;">
</div>
<v-row class="text-center">
<v-col
class="mb-5"
cols="12"
>
<h2 class="headline font-weight-bold mb-3">
Accueil
</h2>
<v-row justify="center">
<p>
Client: {{ JSON.stringify(client)}}
</p>
<p>
Mouse: {{ JSON.stringify(mouse)}}
</p>
<p>
Container: {{ JSON.stringify(container)}}
</p>
</v-row>
</v-col>
</v-row>
</v-container>
</template>
<script>
import * as Three from 'three';
export default {
name: 'Accueil',
mounted() {
this.init();
},
methods: {
init() {
this.createScene();
this.createCamera();
this.userData.formes.forEach((x) => this.createShape(x));
this.addSpotlight(16777215);
this.addAmbientLight();
this.animate();
window.addEventListener('resize', this.onResize);
},
onResize() {
const container = document.getElementById('menu3D');
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.camera.aspect = container.clientWidth / container.clientHeight;
this.camera.updateProjectionMatrix();
},
createScene() {
this.renderer = new Three.WebGLRenderer({
antialias: true,
alpha: true,
});
const container = document.getElementById('menu3D');
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setClearColor(0xffffff, 0);
container.appendChild(this.renderer.domElement);
},
createCamera() {
const container = document.getElementById('menu3D');
this.camera = new Three.PerspectiveCamera(50,
container.clientWidth / container.clientHeight, 0.01, 1000);
this.camera.position.set(0, 5, 20);
this.camera.zoom = 1;
},
createShape(shape) {
const material = new Three.MeshStandardMaterial({
color: '#0000ff',
roughness: 1,
metalness: 0.5,
emissive: 0,
depthFunc: 3,
depthTest: true,
depthWrite: true,
stencilWrite: false,
stencilWriteMask: 255,
stencilFunc: 519,
stencilRef: 0,
stencilFuncMask: 255,
stencilFail: 7680,
stencilZFail: 7680,
stencilZPass: 7680,
});
switch (shape.nom) {
case 'Box': {
this.geometry = new Three.BoxBufferGeometry(1.8, 1.8, 1.8);
break;
}
case 'Sphere': {
this.geometry = new Three.SphereBufferGeometry(1, 8, 6, 0, 6.283185, 0, 3.141593);
break;
}
case 'Dodecahedron': {
this.geometry = new Three.DodecahedronBufferGeometry(1.2, 0);
break;
}
case 'Icosahedron': {
this.geometry = new Three.IcosahedronBufferGeometry(1.5, 0);
break;
}
default: {
return false;
}
}
this.mesh = new Three.Mesh(this.geometry, material);
this.mesh.name = shape.nom;
this.mesh.userData = shape.userData;
this.mesh.receiveShadow = true;
this.mesh.castShadow = true;
this.mesh.position.set(0, shape.userData.position.y, 0);
this.scene.add(this.mesh);
return true;
},
addSpotlight(color) {
const light = new Three.SpotLight(color, 2, 1000);
light.position.set(0, 0, 30);
this.scene.add(light);
},
addAmbientLight() {
const light = new Three.AmbientLight('#fff', 0.5);
this.scene.add(light);
},
verifForme(e) {
const t = this;
const elt = t.scene.getObjectByName(e);
t.intersects = t.raycaster.intersectObject(elt);
if (t.intersects.length !== 0) {
// s'il ne figure pas dans le tableau, on le met en premier
if (t.userData.souris.indexOf(e) < 0) {
t.userData.souris.unshift(e);
console.log(`${t.userData.souris[0]} survolé!`);
}
if (t.userData.souris[0] === e) {
const obj = t.intersects[0].object;
obj.material.color.set(`#${elt.userData.couleurs[1]}`);
obj.scale.set(obj.scale.x < 1.4
? obj.scale.x + t.VITESSE_ZOOM
: obj.scale.x, obj.scale.y < 1.4
? obj.scale.y + t.VITESSE_ZOOM
: obj.scale.y, obj.scale.z < 1.4
? obj.scale.z + t.VITESSE_ZOOM
: obj.scale.z);
obj.rotation.y += t.VITESSE_ROTATION / t.RALENTISSEMENT;
t.replacer(obj, obj.userData.position.y + obj.userData.decalage);
} else {
t.retrecir(e, elt);
}
} else {
if (t.userData.souris.indexOf(e) >= 0) {
t.userData.souris = t.userData.souris.filter((forme) => forme !== e);
}
t.retrecir(e, elt);
}
},
onClick(event) {
event.preventDefault();
if (this.userData.souris.length > 0) {
console.log(`${this.userData.souris[0]} cliqué!`);
} else {
console.log('clic dans le vide!');
}
},
onMouseMove(event) {
const container = document.getElementById('menu3D');
this.mouse.x = (event.offsetX / container.clientWidth) * 2 - 1;
this.mouse.y = -(event.offsetY / container.clientHeight) * 2 + 1;
this.client.clientX = event.clientX;
this.client.clientY = event.clientY;
this.container.width = container.clientWidth;
this.container.height = container.clientHeight;
// console.log(JSON.stringify(this.mouse))
},
replacer(e, py) {
// la ligne suivante est pour éviter les tremblements
if (Math.abs(e.position.y - py) < 0.05) { return true; }
let rhesus = 10 * this.VITESSE_DEPLACEMENT;
if (this.userData.souris[0] !== e.name) { rhesus *= 3; }
// console.log(e.name+': '+this.userData.souris[0]+' - '+rhesus)
if (e.position.y > py) { rhesus = -1; }
e.position.set(0, Math.trunc(10 * e.position.y + rhesus) / 10, 0);
return true;
},
retrecir(n, e) {
// on vérifie si le truc cliqué est dessus
let dec = 0;
const elt = this;
if ((elt.userData.souris.length > 0)
&& (elt.userData.formes.map((x) => x.nom).indexOf(n)
< elt.userData.formes.map((x) => x.nom).indexOf(elt.userData.souris[0]))) {
dec = Math.trunc(10
* e.parent.getObjectByName(elt.userData.souris[0]).userData.decalage
* 2.1) / 10;
}
e.material.color.set(`#${e.userData.couleurs[0]}`);
e.rotation.y += elt.VITESSE_ROTATION;
e.scale.set(e.scale.x > 1
? e.scale.x - elt.VITESSE_ZOOM : e.scale.x,
e.scale.y > 1
? e.scale.y - elt.VITESSE_ZOOM : e.scale.y,
e.scale.z > 1
? e.scale.z - elt.VITESSE_ZOOM : e.scale.z);
const newY = e.userData.position.y + dec;
if (e.position.y !== newY) {
elt.replacer(e, newY);
}
},
animate() {
const elt = this;
requestAnimationFrame(this.animate);
this.raycaster.setFromCamera(this.mouse, this.camera);
this.userData.formes.map((x) => x.nom).forEach((x) => elt.verifForme(x));
if (this.userData.souris.length > 0) {
document.body.style.cursor = 'pointer';
} else { document.body.style.cursor = 'default'; }
this.camera.updateProjectionMatrix();
this.renderer.render(this.scene, this.camera);
},
},
data: () => ({
container: { height: 0, width: 0 },
client: { clientX: 0, clientY: 0 },
scene: new Three.Scene(),
camera: null,
renderer: Three.WebGLRenderer,
mesh: new Three.Mesh(),
factor: 0,
mouse: new Three.Vector2(1, 1),
raycaster: new Three.Raycaster(),
intersects: [],
VITESSE_ROTATION: 0.05,
VITESSE_DEPLACEMENT: 0.1,
VITESSE_ZOOM: 0.05,
RALENTISSEMENT: 3,
userData: {
souris: [],
formes: [
{
nom: 'Box',
userData: {
position: {
x: 0,
y: 7.8,
z: 0,
},
couleurs: [
'aaaaaa',
'095256',
],
decalage: 0.5,
},
},
{
nom: 'Icosahedron',
userData: {
position: {
x: 0,
y: 5.5,
z: 0,
},
couleurs: [
'aaaaaa',
'087F8C',
],
decalage: 0.5,
},
},
{
nom: 'Dodecahedron',
userData: {
position: {
x: 0,
y: 3.1,
z: 0,
},
couleurs: [
'aaaaaa',
'5AAA95',
],
decalage: 0.4,
},
},
{
nom: 'Sphere',
userData: {
position: {
x: 0,
y: 1,
z: 0,
},
couleurs: [
'aaaaaa',
'86A873',
],
decalage: 0.2,
},
},
],
},
}),
};
</script>
And here is the error I have in the console with VueJS 3:
three.module.js?5a89:24471 Uncaught TypeError:
'get' on proxy: property 'modelViewMatrix' is a read-only and
non-configurable data property on the proxy target but the proxy did not
return its actual value (expected '#<Matrix4>' but got '[object Object]')
at renderObject (three.module.js?5a89:24471)
at renderObjects (three.module.js?5a89:24458)
at Proxy.WebGLRenderer.render (three.module.js?5a89:24258)
at animate (HelloWorld.vue?fdab:192)
If anyone has got a clue, thanks in advance...
It worked with Vue 2
Reason it worked fine with Vue 2 lies in the fact Vue 2 is using different reactivity system based on Object.defineProperty API.
The same API is used by THREE.js a lot to add some non-writable and non-configurable properties to it's data structures
When object with such property was passed to Vue (by declaring it inside data for example), Vue just skipped such property resulting in stored value/object being non-reactive (as Vue could not detect property access while rendering the component template)
Vue 3 proxies
Vue 3 is using new reactivity system base on ES6 proxies.
This is pretty new and even that a lot of effort has been put into developing and testing it, issues like this will arise as people start migrating (And I completely agree with #Serg - Vue 3 is still new and unless you have skill and time to "live on the edge" you should wait a bit before migrating from Vue 2)
This new reactivity system doesn't play well with non-writable non-configurable properties on objects - you can find minimal reproducible example in this sandbox
Imho it is a bug and is reported to Vue#next repo
sandbox uses composition API but that doesn't matter as using reactive() is the same as declaring your variables inside data() function (Vue just do it automatically for you)
Workarounds
As said before, problem is in reactivity system. I'm not an expert on THREE.js but from what I know it doesn't make much sense to put the THREE data structures into Vue reactivity system - all point of reactivity is to detect data changes and re-render template when needed. THREE has its own rendering system and is usually using single <canvas> HTML element so it makes no sense to trigger Vue re-render on THREE data structures change...
There are multiple ways to opt-out from Vue reactivity:
Use Object.freeze() on your objects. Not very useful in this case but good to know
Do not declare your variables in data() and assign the values in created()/mounted() hook (example bellow). You can assign them into component itself (this) if you need to access them in multiple methods or as a local variables (const/let) whenf you don't need it
When using Composition API, do not use reactive() on THREE data structures
NOTE: Even if they admit it is a bug, only way of fixing it is to leave such property and object it holds non-reactive (not putting Proxy around that object) so result will be same as opting-out of reactivity completely. But using this workaround also gives you faster and less memory hungry app as all the reactivity is not really that cheap
Example - creating non-reactive component properties
export default {
data() {
return {
};
},
mounted() {
this.init();
},
methods: {
init() {
this.scene = new THREE.Scene();
this.camera = new THREE.OrthographicCamera(...);
this.renderer = new THREE.WebGLRenderer({ ... })
this.geometry = new THREE.PlaneBufferGeometry( );
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
this.plane = new THREE.Mesh(this.geometry, material);
this.scene.add(this.plane);
this.renderer.render(this.scene, this.camera);
},
}
toRaw(vue3) - At this time, you can feel his strength !
You can use this method, to solve a series of these problems
If mesh/xxx is a ref variable
scene.add(toRaw(mesh.value))
renderer.value.render(toRaw(scene.value), camera.value);
I am using threejs + vue3 + pinia. Pinia was wrapping objects in Proxy too, but I need to pass 3d object to it sometimes (inside if other model). So I had a model like:
class SomeModel {
otherProp: 'some value',
graphicObject: new THREE.Object3D(),
}
The way I fixed this issue is by changing graphicObject prop to a function, that return 3d object, that was saved in other variable. It looks like this:
class SomeModel {
otherProp: 'some value',
constructor(graphicObject) {
this.graphicObject = () => graphicObject,
}
}
new SomeModel(new THREE.Object3D());
This way 3d object is hidden from Vue at all time, if you dont pass this object directly to any reactive variable. And the way you access it in other methods is by just calling this function like in example:
<script setup>
import { ref } from 'vue';
// You may just call constructor inside new SomeModel() if you want.
const graphicObject = new THREE.Object3D();
const someModel = ref(new SomeModel(graphicObject));
function changePosition(x, y, z) {
// should not emit errors, because 3d object is not reactive -
// it's just a value, returned from function.
someModel.value.graphicObject().position.set(x, y, z);
}
</script>

Prevent Vue Multiple Select to Store an Empty Array

I want this select multiple to pre-select one option, and not be able to deselect all options.
Whenever the last selected option is deselected it should be reselected. In other words when the user tries to deselect the last selected option it should visually not be deselected.
<template>
<b-select
if="Object.keys(doc).length !== 0 /* wait until firebase has loaded */"
:options="computedOptions"
v-model="model"
multiple
#input="onChange"
/>
</template>
<script>
//import Vue from 'vue'
import { fb } from "../fbconf";
export default {
name: "MyMultiSelect",
props: {
doc: Object, // firestore document
},
data() {
return {
options: []
};
},
firestore() {
var options = fb.db.collection("options");
return {
options: options
};
},
computed: {
computedOptions: function() {
return this.options.map(function(option) {
return {
text: option.name,
value: option.id
};
});
},
// to make sure mySelectedOptions is an array, before this.doc is loaded
// I use the following custom model
// because not using 'get' below causes a warning:
// [Vue warn]: <select multiple v-model="localValue"> expects an Array value for its binding, but got Undefined
model: {
get: function() {
if (!this.doc.hasOwnProperty('mySelectedOptions')) return []; // empty array before this.doc is loaded
else return this.doc['mySelectedOptions'];
},
set: function(newValue) {
// here I can prevent the empty array from being stored
// but visually the user can deselect all options, which is bad UX
//if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
}
},
},
methods: {
onChange: function(newValue){
// I can manually store the array as I want here
// but I cannot in any way prevent the user from deselecting all options
if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
else {
// none of these reselects the last selected option
var oldValue = this.doc['mySelectedOptions'];
this.doc['mySelectedOptions'] = this.doc['mySelectedOptions'];
//this.$forceUpdate();
//this.$emit("change", newValue);
//Vue.set(this.doc, 'mySelectedOptions', this.doc['mySelectedOptions']);
}
}
}
};
</script>
You could add watcher and when length becomes 0 just add previous value.
watch: {
model(val, oldVal) {
if(val.length == 0 && oldVal.length > 0) {
// take only one item in case there's clear button or etc.
this.model = [oldval[0]];
}
}
}

Vue v-for list not re-rendering after computed data update

I am implementing pagination for a huge list of cards, I display 10 cards at once and wish to show the 10 next (or 10 previous) by clicking on two buttons.
Here's how I do it:
export default {
...
data() {
return {
pois: [], // My list of elements
pageNumber: 0, // Current page number
};
},
props: {
size: {
type: Number,
required: false,
default: 10, // 10 cards per page
},
},
computed: {
pageCount() {
// Counts the number of pages total
const l = this.pois.length;
const s = this.size;
return Math.floor(l / s);
},
paginatedData() {
// Returns the right cards based on the current page
const start = this.pageNumber * this.size;
const end = start + this.size;
return this.pois.slice(start, end);
},
},
methods: {
nextPage() {
this.pageNumber += 1;
},
prevPage() {
this.pageNumber -= 1;
},
}
...
};
And my template:
<div v-for="poi in paginatedData" :key="poi.id">
<card :poi="poi"/>
</div>
Everything should work (and a page change does output the correct cards in the console) but my list is not updated even though the computed method is called on each click.
What is causing this issue? I've read it could be linked to a :key value missing, but it's there, and no data is being updated directly and manually in the array, only sliced out.
First, try this change, just for sure, and let me know in comment it works or not.
export default {
...
computed: {
paginatedData() {
...
const end = start + this.size - 1;
...
},
...
};
And yes: instead of id, try to use index:
<div v-for="(poi, idx) in paginatedData" :key="idx">
<card :poi="poi"/>
</div>

Animating dimensions in Sencha Touch 2

I'm trying to animate the height of a dataview, but it's currently just sliding the panel around the viewport instead of keeping it in place and changing it's height. The code is as follows:
Ext.Anim.run(el, 'slide', {
from: { height: height },
to: { height: newHeight },
out: false,
direction: 'up',
easing: 'ease-out',
duration: 1000
});
For instance, height=200, newHeight=100 will result in the dataview dropping immediately so that it's top is at 200px below the viewport, and then animating back to the top of the viewport.
How can I get it to change the height? Thanks.
Try using Ext.Animator.run instead:
Ext.Animator.run({
element: dataview.element,
duration: 500,
easing: 'ease-in',
preserveEndState: true,
from: {
height: dataview.element.getHeight()
},
to: {
height: 100
}
});
And within a full example:
Ext.application({
name: 'Sencha',
launch: function() {
var dataview = Ext.create('Ext.DataView', {
fullscreen: true,
style: 'background:red',
store: {
fields: ['text'],
data: [
{ text: 'one' },
{ text: 'two' },
{ text: 'three' }
]
},
itemTpl: '{text}'
});
Ext.Viewport.add({
xtype: 'button',
docked: 'top',
handler: function() {
Ext.Animator.run({
element: dataview.element,
duration: 500,
easing: 'ease-in',
preserveEndState: true,
to: {
height: 100
},
from: {
height: dataview.element.getHeight()
}
});
}
});
}
});
Since I can't add comments, I'll have to put this as a separate answer. I just wanted to add to what rdougan said and show how you can catch the animation end event. I find it's necessary in the above situation because Sencha Touch's component.getTop/Left/Height/Width() functions return incorrect values after an animation such as the one shown.
dataview.setHeight(dataview.element.getHeight()); // you may or may not need this
console.log('height before\t', dataview.getHeight());
var a = new Ext.fx.Animation({
element: dataview.element,
duration: 500,
easing: 'ease-in',
preserveEndState: true,
from: {
height: dataview.element.getHeight()
},
to: {
height: 100
}
});
a.on('animationend', function (animation, element, isInterrupted) {
console.log('height before\t', dataview.getHeight());
dataview.setHeight(dataview.element.getHeight());
console.log('height set\t', dataview.getHeight());
});
Ext.Animator.run(a);
I left in some logging so you can see just what I mean. This example was written against ST 2.1 RC2.
Here's a clean utility function you can use to accomplish this
function animatron (target, prop, duration, to, from, easing) {
// return if no target or prop
if (target == null || prop == null) { return; }
// defaults
if (duration == null) { duration = 250; }
if (to == null) { to = 0; }
if (from == null) { from = target.getHeight(); }
if (easing == null) { easing = 'ease-out'; }
// to property
var t = {};
t[prop] = to;
// from property
var f = {};
f[prop] = from;
// Animation Options
var opts = {
duration: duration,
easing: easing,
element: target.element,
from: f,
preserveEndState: true,
to: t
};
// Animation Object
var anime = new Ext.fx.Animation(opts);
// On animationend Event
anime.on('animationend', function (animation, element, isInterrupted) {
// Hide the target if the to is 0
if (to == 0 && (prop == 'height' || prop == 'width')) {
if (!isInterrupted) { target.hide(); }
}
// Update property if width or height
if (prop == 'height') { target.setHeight(to); }
if (prop == 'width') { target.setWidth(to); }
// Dispatch 'animated' event to target
target.fireEvent('animated', animation, element, to, from, isInterrupted);
});
// Show the target if it's hidden and to isn't 0
if (target.getHidden() == true && to != 0) { target.show(); }
// Run the animation
Ext.Animator.run(anime);
}
You can listen for the 'animated' event on the target element
animatron(dataview, 'height', 500, 0);
dataview.addListener('animated', function (animation, element, to, from, isInterrupted) {
console.log('animation ended');
console.log('interrupted: '+ isInterrupted);
});