PDA

View Full Version : Container.getEl() returns undefined causing the script to halt execution



momesana
16 Oct 2010, 7:49 PM
Hi,
I am trying to implement a custom component and have some issues with getEl() returning an undefined reference. I am accessing the dom element from within afterRender() so I thought by that time the container has been added to the dom thus containing a valid reference to the dom element. But the following code causes the following error on chrome:
Uncaught TypeError: Cannot call method 'getWidth' of undefined
Can someone tell me what to do so this doesn't happen again? When can I be sure that the component has been added to the dom and that the references to the dom elements are valid?

Here is the code in a selfcontained index.html file (make sure there is a test.png file in the same directory where the html file is placed):


<html>
<head>
<title></title>
<style type="text/css">@import "libs/sencha-touch/resources/css/ext-touch.css";</style>
<style type="text/css">
.screen {
background-color: rgb(220,220,220);
background-image: none;
}
</style>
<script type="text/javascript" src="libs/sencha-touch/ext-touch-debug.js"></script>
<!-- <script type="text/javascript" src="js/sencha-touch.js"></script> -->

<script type="text/javascript">
String.prototype.isEmpty = function() {
return (this == '');
}

function getRandomColor() {
var min = 220; // don't set min to a value greate than 255
var ar = new Array();
for (var i = 0; i < 3; ++i) {
ar.push(min + parseInt(Math.random() * (255 - min)));
}
var color = 'rgb(' + ar.join(",") + ')';
return color;
}

Ext.setup({
fullscreen: true,
statusBarStyle: 'black-translucent',
icon: 'icon.png',
tabletStartupScreen: 'tablet_startup.png',
phoneStartupScreen: 'phone_startup.png',
glossOnIcon: true,
onReady: function() {


VideoPlaybackWidget = Ext.extend(Ext.Panel, {
initComponent: function() {

var beforeVideoCnt = new VideoPreviewWidget({
height: 320,
style: {
border: '1px solid red'
}
});
beforeVideoCnt.setSource('test.png');

// Initializing component
Ext.apply(this, {
layout: {
type: 'fit'
},
defaults: {
layout: {
align: 'stretch'
}
},
items: [{
xtype: 'container',
items: [beforeVideoCnt]

}]
});

VideoPlaybackWidget.superclass.initComponent.apply(this, arguments);
}
});

Ext.reg('videoplayblackwidget', VideoPlaybackWidget);

VideoPreviewWidget = Ext.extend(Ext.Container, {
initComponent: function() {

this.source = '';

/**
* @param: s is the new source of the image
*/
this.setSource = function(s) {
if (s == undefined) {
// assign nothing to source
}
else if (typeof s != 'string') {
console.error('TypeError');
return;
}
else {
this.source = s;
}

function waitUntilLoaded(scope) {
if (!img.complete) {
console.log('scheduled for later');
setTimeout(waitUntilLoaded, 100, scope);

} else {

var w = img.width;
var h = img.height;

var aspectRatio = scope.getWidth() / scope.getHeight();
if (w == 0 || h == 0) {
w = scope.getWidth();
h = scope.getHeight();
} else {
var aspectRatio = w / h;
}

if (w > scope.getWidth()) {
w = scope.getWidth();
h = w / aspectRatio;
}

if (h > scope.getHeight()) {
h = scope.getHeight();
w = h * aspectRatio;
}

var el = scope.getEl();
if (el) {
var imgEl = el.down('img');
imgEl.set({
style: (scope.source == null || scope.source.isEmpty()) ? 'display:none;' : 'display: block;',
src: scope.source,
alt: 'image path:' + scope.source,
width: (w == undefined) ? 'auto' : w,
height: (h == undefined) ? 'auto' : h
});
repositionElements(imgEl, w, h, scope);
}
}
};

var img = new Image();
img.src = this.source;
waitUntilLoaded(this);
};

this.getSource = function() {
return this.source;
};

function repositionElements(el, w, h, scope) {

var xoffset = Math.floor((scope.width - w) / 2);
var yoffset = Math.floor((scope.height - h) / 2);

var el = scope.getEl();
if (el) {
var imgEl = el.down('img');
console.log('left:' + xoffset + 'px; top:' + yoffset + 'px;');

imgEl.set({
style: 'left:' + xoffset + 'px; top:' + yoffset + 'px;'
});
}
};

this.test = function(obj) { // DEBUG
console.log('on show');
};


/* Initializing component */
Ext.apply(this, {
html: '<div style="position:relative;"><img style="position: absolute;"/><div style="position: absolute;"></div></div>'
});

VideoPreviewWidget.superclass.initComponent.apply(this, arguments);
},

afterRender: function() {
VideoPreviewWidget.superclass.afterRender.apply(this, arguments);
setTimeout(this.setSource(), 10000);
}
});

Ext.reg('videopreviewwidget', VideoPreviewWidget);

var screen = new VideoPlaybackWidget({
fullscreen: true
});
screen.show();

}
});


</script>
</head>

<body>
</body>
</html>

Animal
17 Oct 2010, 1:21 AM
Apart from anything, you are overnesting.

Your main, "fullscreen" Panel contains a Container which contains a VideoPreviewWidget.

It should just contain a VideoPreviewWidget



// Initializing component
Ext.apply(this, {
layout: {
type: 'fit'
},
items: [beforeVideoCnt]
});


As to your problem. There will not be an Element to get until the Panel has been rendered.

Your VideoPreviewWidget is bizarre. You create a new Image(), and on load, you set the original imgEl's src to the source? Why not just set the imgEl's src right away (Well, onRender anyway!)

momesana
17 Oct 2010, 5:09 AM
Apart from anything, you are overnesting.

Your main, "fullscreen" Panel contains a Container which contains a VideoPreviewWidget.

It should just contain a VideoPreviewWidget



// Initializing component
Ext.apply(this, {
layout: {
type: 'fit'
},
items: [beforeVideoCnt]
});


As to your problem. There will not be an Element to get until the Panel has been rendered.

Your VideoPreviewWidget is bizarre. You create a new Image(), and on load, you set the original imgEl's src to the source? Why not just set the imgEl's src right away (Well, onRender anyway!)
Hi,
thank you for replying. Well, that example I sent you is actually a simplified version. The original version is much more nested than this one but of course there is more than one component inside the layout. I jus took away the other elements for this example to make the code more readable.

The new Image is just for reading out the width/height of the Image in order to be able to center align the image on the container. I want to be able to set the image right away when instantiating the object, i.e. before it has been added to any other container and displayed. By that time the actual size of the Container (for example it's width when align: stretch has been passed) is unknown since it hasn't been yet rendered. Container.getEl() will also return undefined so I can't set any attributes anyway. So I want the setSource function to just set the string variable this.source to the specified source and drop out of the function.

By the time the widget is rendered however - and I thought that would be when afterRender is executed - I expect the dom elements to be in place and want to do the processing so this time setSource is called again without a parameter in which case it defaults back to this.source. So it should be able to set the attributes of the image element now since the getEl() returns a valid reference. When using onRender it seems like even getEl() is defined, the image element I defined in the html property are still null so I cant call set on them. That's why I resorted to afterRender().

I have changed the code a little so I have a separate function updateComponent that is called when I call setSource and also from withing afterRender. The problem now is that Container.getWidth() and Container.getHeight() provide incorrect values. In Chrome's debugger I see that the container has a width of about 950px. Container.getWidth() however returns some values like 2pixels. Shouldn't getWidth() provide the actual width when called from withing afterRender()?

Thanks in advance



VideoPreviewWidget = Ext.extend(Ext.Container, {
initComponent: function() {

this.source = '';

this.updateComponent = function(scope) {
if (scope.getEl() == undefined || scope.getEl() == null)
return;

var img = new Image();
img.src = scope.source;
waitUntilLoaded(scope);

function waitUntilLoaded(scope) {
if (!img.complete) {
console.log('image not loaded yet');
setTimeout(waitUntilLoaded, 100, scope);

} else {
var w = img.width;
var h = img.height;
cntW = scope.getWidth(); // container width
cntH = scope.getHeight(); // container height

var aspectRatio = cntW / cntH;
if (w == 0 || h == 0) {
w = cntW;
h = cntH;
} else {
var aspectRatio = w / h;
}

if (w > cntW) {
w = cntW;
h = w / aspectRatio;
}

if (h > cntH) {
h = cntH;
w = h * aspectRatio;
}

var el = scope.getEl();
if (el) {
var imgEl = el.down('img');

imgEl.set({
style: (scope.source == null || scope.source.isEmpty()) ? 'display:none;' : 'display: block;',
src: scope.source,
alt: 'image path:' + scope.source,
width: (w == undefined) ? 'auto' : w,
height: (h == undefined) ? 'auto' : h
});
repositionElements(imgEl, w, h, scope);
}
}
};
}

this.setSource = function(s) {
if (s == undefined || typeof s != 'string')
return;

this.source = s;
this.updateComponent(this);
};

this.getSource = function() {
return this.source;
};

function repositionElements(el, w, h, scope) {

var xoffset = Math.floor((scope.getWidth() - w) / 2);
var yoffset = Math.floor((scope.getHeight() - h) / 2);

console.log(scope.getWidth() + ', '+ scope.getHeight());
var el = scope.getEl();
if (el) {
var imgEl = el.down('img');
console.log('left:' + xoffset + 'px; top:' + yoffset + 'px;');

imgEl.set({
style: 'left:' + xoffset + 'px; top:' + yoffset + 'px;'
});
}
};

/* Initializing component */
Ext.apply(this, {
html: '<div style="position:relative;"><img style="position: absolute;"/><div style="position: absolute;"></div></div>'
});

VideoPreviewWidget.superclass.initComponent.apply(this, arguments);
},

afterRender: function() {
VideoPreviewWidget.superclass.afterRender.apply(this, arguments);
this.updateComponent(this);
}
});

Ext.reg('videopreviewwidget', VideoPreviewWidget);

evant
17 Oct 2010, 9:57 PM
It's kind of an odd way to write an extended class. What are you trying to achieve?

momesana
17 Oct 2010, 11:44 PM
It's kind of an odd way to write an extended class. What are you trying to achieve?
Well, actually I am just trying to implement a Container that displays a center-aligned image that is resized to the fit into the container (if the image size exceeds the bounds of the container) while keeping it's aspect ratio. The problem is probably that I am new to javascript and Sencha in general and don't know how to subclass properly and override the default behaviour etc. So far I've found only one document describing the process of subclassing in Sencha using Ext.extend() (http://www.sencha.com/learn/Tutorial:Creating_new_UI_controls) but the proper process of doing this is still more or less unclear to me. I have a background in C++/Qt and there I would simply introduce a Constructor, pass some arguments to the superclass constructor and add methods to the class and/or override event handlers. In Sencha we have initComponent(), constructor() and all those template methods that need to be overridden. It's all a little confusing. I've got my code to work somehow (see the pasted code below) but it's definitely far from perfect. It would be nice to get some feedback/advice about what I am doing wrong and what the proper way of doing this would like.

Thanks in advance



VideoPreviewWidget = Ext.extend(Ext.Container, {
initComponent: function() {
this.source = '';
this.dirty = false;

this.alignElements = function(img) {
if (this.getEl() == undefined || this.getEl() == null) {
console.log('warning: alignElements(): el undefined!');
return;
}

var imgEl = this.getEl().down('img');

var w = img.width;
var h = img.height;

var cntW = this.getEl().getWidth(true); // container width
var cntH = this.getEl().getHeight(true); // container height
console.info('image size:' + w + 'x' + h);
console.info('container size:' + cntW + 'x' + cntH);

var aspectRatio = cntW / cntH;
if (w == 0 || h == 0) {
w = cntW;
h = cntH;
} else {
var aspectRatio = w / h;
}

if (w > cntW) {
w = cntW;
h = w / aspectRatio;
}

if (h > cntH) {
h = cntH;
w = h * aspectRatio;
}

w = Math.round(w);
h = Math.round(h);

var styleWidth = 'width:' + ((w == undefined || w == NaN) ? 'auto; ' : w + 'px; ');
var styleHeight = 'height:' + ((h == undefined || h == NaN) ? 'auto; ' : h + 'px; ');

console.info('adjusted image size:' + w + 'x' + h); // DEBUG
var xoffset = Math.round((cntW - w) / 2);
var yoffset = Math.round((cntH - h) / 2);

var styleLeft = 'left:' + ((xoffset == undefined || xoffset == NaN) ? 'auto; ' : xoffset + 'px; ');
var styleTop = 'top:' + ((yoffset == undefined || yoffset == NaN) ? 'auto; ' : yoffset + 'px; ');

var dsp = (this.source == null || this.source.isEmpty()) ? 'display:none; ' : 'display:block;';
var style = styleWidth + styleHeight + styleLeft + styleTop + dsp;

console.info(style); // DEBUG
imgEl.set({
style: style,
src: this.source,
alt: 'image path:' + this.source
});

imgEl.set({
style: 'left:' + xoffset + 'px; top:' + yoffset + 'px;'
});
}

this.setSource = function(s) {
if (s == undefined || typeof s != 'string') return;

this.source = s;

var el = this.getEl();
if (this.getEl() == undefined || this.getEl() == null) this.dirty = true;
else this.setImage();
};

this.setImage = function() {
var preloadImg = new Image();

el = this;
preloadImg.onload = function() { el.alignElements(preloadImg); };
preloadImg.src = this.source;

this.dirty = false;
}

this.getSource = function() {
return this.source;
};

function repositionElements(el, w, h, scope) {

};

/* Initializing component */
Ext.apply(this, {
html: '<div style="position:relative;"><img style="position: absolute;"/><div style="position: absolute;"></div></div>'
});

VideoPreviewWidget.superclass.initComponent.apply(this, arguments);
},

afterRender: function() {
VideoPreviewWidget.superclass.afterRender.apply(this, arguments);
var el = this.getEl().down('img');
if (this.dirty) this.setImage();
}
});


Ext.reg('videopreviewwidget', VideoPreviewWidget);