I was attempting to migrate a custom view component to 3.0 and frustrated.
The new type PercentLength makes great sense and appears to return a readonly value property however it doesn't appear we can get access to it. TypeScript 2.2 throws error when trying to access.
The Migration guide stated here:
https://github.com/NativeScript/NativeScript/blob/master/Modules30Changes.md#property-types
makes mention of:
You will have to be careful when getting the value - you might get an complex object instead of number
N/p but how to get the value?
Before you could simply do this in a custom component which extended the View super class:
export class CustomComponent extends View {
// custom implementation...
public onLoaded() {
super.onLoaded();
console.log(`XML: dimensions ${this.width}x${this.height}`);
}
}
With 3.0, this now prints:
XML: dimensions [object Object]x[object Object]
...As mentioned in the docs, n/p. But when you try to access the value as supposedly the new type PercentLength has defined:
console.log(`XML: dimensions ${this.width.value}x${this.height.value}`);
This throws TypeScript errors:
Property 'value' does not exist on type 'PercentLength'.
However upon inspecting PercentLength, it defines a union type:
export type PercentLength = "auto" | number | {
readonly unit: "%" | "dip" | "px";
/**
* Length value. When unit is "%" the value is normalized (ex. for 5% the value is 0.05)
*/
readonly value: number;
}
However it doesn't appear we can access value? Am I missing something? I just need simple access to the width value.
I have also tried importing PercentLength and attempting to use the static method, ie:
let internalWidth = PercentLength.toDevicePixels(this.width, 0, 0);
But that also returns an object?
This is frustrating and hope this can be cleared up in the migration docs :(
Can anyone help shed some light here?
Using:
| Component | Current version | Latest version | Information |
| ------------- |:-------------:| -----:| -----:|
| nativescript | 3.0.0-rc.1 | 2.5.4 | Up to date |
| tns-core-modules | 3.0.0-rc.2 | 2.5.2 | Up to date |
| tns-android | | 2.5.0 | Not installed |
| tns-ios | 3.0.0-rc.1 | 2.5.0 | Up to date |
From my experience the correct way is to use the static toDevicePixels() method. I have used it here here and it is working, not sure why are you getting an [Object] for the internalWidth value.
Also I think you can use this.effectiveWidth value. This should be a number of device pixels. I'm just not sure if that will be calcualted and available in the onLoaded sub...
Thank you @PeterStaev ok so it looks like let internalWidth = PercentLength.toDevicePixels(this.width, 0, 0); is actually working now although it's width value is not correct (does not equal what is explicitly defined on the xml). Curious though, is that the new 3.0 way to determine component width/height? Just checking.
@NathanWalker , I think so. At least from my looks at the core modules this is how it is done. May be try to use the effectiveWidth value and see if that will return the correct value. But might be best for someone on the {N} team to confirm this :)
@PeterStaev I am pretty shocked at how difficult this has proven to be with the new changes.
I still am unable to simply get the explicit width and height attributes from the component xml given:
<Custom:Component width="188" height="115">
</Custom:Component>
I have found no way to determine what width and height are in the component model?
public onLoaded() {
super.onLoaded();
console.log(`XML: dimensions ${this.width}x${this.height}`); // INCORRECT!
}
No Go 馃槥
public onLoaded() {
super.onLoaded();
console.log(`XML: dimensions ${this.width.value}x${this.height.value}`); // TypeScript errors!
}
No Go 馃槥
public onLoaded() {
super.onLoaded();
let xmlWidth = PercentLength.toDevicePixels(this.width, 0, 0);
let xmlHeight = PercentLength.toDevicePixels(this.height, 0, 0);
console.log(`XML: dimensions ${xmlWidth}x${xmlHeight}`); // INCORRECT!
}
No Go 馃槥
public onLoaded() {
super.onLoaded();
let xmlWidth = PercentLength.toDevicePixels(this.effectiveWidth, 0, 0);
let xmlHeight = PercentLength.toDevicePixels(this.effectiveHeight, 0, 0);
console.log(`XML: dimensions ${xmlWidth}x${xmlHeight}`); // INCORRECT!
}
No Go 馃槥
At this point I'm highly disappointed and frustrated. How to simply get the height and width defined explicitly on the XML with new 3.0 changes? Please?
/cc @hdeshev @vakrilov @vchimev @sis0k0 ^ Calling in for help :)
@NathanWalker , it does not make sense to get the pure value of 188 in your case for width. What if I set the width to 22%? So with the PercentLength and Length objects you must calculate the number using the toDevicePixels function. Now note that this function returns device pixels and NOT device independent pixels. So in your case the 188 width you set in XML is actually 188dp. When you call toDevicePixels this will return the value of the width multiplied by the screen density. For example for iPhone you will get 188 * 2= 376 for iPhone Plus you will get 188 * 3. Specifically for iOS setting the frame/size of native objects is based on DP (from what I know). So in order to get the DPs back from that calculated value you must use utils.layout.toDeviceIndependentPixels() function.
And one further note: The effectiveWidth and effectiveHeight values are already calculated using toDevicePixels. So you do not need to apply the function to those.
With this said try the following:
import * as utils from "utils/utils";
public onLoaded() {
super.onLoaded();
let dpWidth= utils.layout.toDeviceIndependentPixels(this.effectiveWidth);
let dpHeight = utils.layout.toDeviceIndependentPixels(this.effectiveHeight);
console.log(`XML: dimensions ${dpWidth}x${dpHeight}`);
}
@NathanWalker Could you share your use case? Like @PeterStaev said there is little need to use the effective value directly. More the PercentLength effectiveValue is calculated when measure/layout pass is done so it won't be calculated in loaded event.
Some details:
Android works in device pixels. It supports device independent pixels (and few other dimensions like - px, dp, sp, etc.) set in XML but converts them immediately to device pixels. So all values (for example in measure/layout) are in device pixels.
IOS on the other side works with device independent pixels (dip). So when you set the native frame field you need to convert the values to dip.
With tns-core-modules 2.* we supported only dip. With 3.0 we added support for device pixel (px) as well. That is why width, height are no longer pure numbers. In core-modules we choose to work with device pixels (like in android) so measure/layout is done in px. That is why we convert width to pixels and store it in effectiveWidth. Same for margins, borderWidth, etc.
The one that are of type PercentLength requires parent available length so the must be calculated in the measure pass. The other that are of type Length could be converted to pixels immediately (and in fact we do it and store the value in effective* fields for faster access).
So in short - PercentLength.toDevicePixels requires:
auto value - numeric value that should be used if you set width='auto' in xml/csswidth='25%)Length.toDevicePixels requires:
auto value - numeric value that should be used if you set width='auto' in xml/cssBoth will convert dip to px and ceil the value.
Our layout implementation will take care of these details internally so you shouldn't have to mess with it.
If you want to assign width from one element to another you could do it directly:
let btn = page.getViewById<Button>('myBtn');
let label = new Label();
label.width = btn.width;
There is also utils.layout.toDeviceIndependentPixels and utils.layout.toDevicePixels that simply multiply/divide to screenScale/screenDensity but they should not be used to convert Length/PercentLength to device pixels.
@hshristov Thank you very much for this detailed explanation, please consider adding it to the migration guide. I will try your suggestions tomorrow!
As far as use case, it simple - sometimes a custom component particularly on iOS uses a custom view controller which needs to allow the user to specify precise width and height via the component. The value must be direct and precisely the values specified by the xml.
The more I reflect on this API, the more I think the height and width properties should behave like the browser APIs (consistent with our goals of being familiar to web developers).
Along those lines, I suppose it makes sense and is correct for width and height to return the range of possible values (i.e. "50%" or "150px" or "auto"). This is how these APIs work in the browser.
Similarly, while you would use .offsetWidth or .offsetHeight to get actual height/width at runtime, I suppose it does make sense to use a similar API in {N} (.effectiveHeight).
We just need a jQuery-like API to simplify for people that want to do element.width() :)
@toddanglin I agree here however I think the jQuery-like api things could land in community space by expanding this plugin https://github.com/NathanaelA/nativescript-dom to support width and height to provide those familiar paradigms.
I can understand core team decision here since native width/height is different in the native api space with the various devices however I don't like going forward with 3.0 without an equivalent way to handle widht/height like we were pre-3.0, doesn't seem necessary since it worked fine pre-3.
The fact you cannot just grab an explicit value the user defined in the XML like width/height seems wrong and very confusing.
Agreed. Per "works like the browser" logic, you should be able to get the user defined value (as a string?) at least by accessing the style property.
element.style.width should return whatever the user configured (i.e. "auto" or "150px" or "150" or whatever). I think this is what you'd get with JavaScript in the browser.
And if you want the actual, runtime values, you'd use the .effectiveWidth/Height property. We just need to document when in the View lifecycle the .effectiveWidth/Height property should be available (when do the layout/measure passes complete?).
@hshristov
There is also utils.layout.toDeviceIndependentPixels and utils.layout.toDevicePixels that simply multiply/divide to screenScale/screenDensity but they should not be used to convert Length/PercentLength to device pixels.
@PeterStaev appears to suggest using:
onLoaded() {
super.onLoaded();
let dpWidth= utils.layout.toDeviceIndependentPixels(this.effectiveWidth); // always 0
let dpHeight = utils.layout.toDeviceIndependentPixels(this.effectiveHeight); // always 0
console.log(`XML: dimensions ${dpWidth}x${dpHeight}`); // 0x0
}
Which doesn't work unfortunately, effectiveWidth and effectiveHeight inside onLoaded return 0/0 :(
However @hshristov you suggest not using those to do that? What should be used instead?
Again, just trying to get the actual number the user specified on the custom component XML, nothing more.
@hshristov @PeterStaev Ok found a solution! This works - instead of checking it in onLoaded, using onLayout as Hristo suggested:
More the PercentLength effectiveValue is calculated when measure/layout pass is done so it won't be calculated in loaded event.
onLayout(left, top, right, bottom) {
super.onLayout(left, top, right, bottom);
console.log(`XML: dimensions ${this.effectiveWidth}x${this.effectiveHeight}`);
let dpWidth= utils.layout.toDeviceIndependentPixels(this.effectiveWidth);
let dpHeight = utils.layout.toDeviceIndependentPixels(this.effectiveHeight);
console.log(`XML: dimensions ${dpWidth}x${dpHeight}`); // correct! exactly what user specified
}
This works for my use case even though it was mentioned not to use those for this purpose, but it does exactly what I need. Curious to hear why more from @hshristov on why:
...but they should not be used to convert Length/PercentLength to device pixels.
Btw, all the above would be great to add to migration guide - sizing and getting dimensions is a critical point to custom component development.
Ok further I think what @hshristov is suggesting is correct and perhaps no confusion here (definitely a rather confusing topic - some clear documentation around this [would be advantageous to be lengthy] to ensure clarity might be good).
So the following is good for iOS since as mentioned:
IOS on the other side works with device independent pixels (dip). So when you set the native frame field you need to convert the values to dip.
let dpWidth= utils.layout.toDeviceIndependentPixels(this.effectiveWidth);
let dpHeight = utils.layout.toDeviceIndependentPixels(this.effectiveHeight);
console.log(`XML: dimensions ${dpWidth}x${dpHeight}`); // correct! exactly what user specified
// can set iOS frame correctly with these converted values now.
Good and fine to use for iOS?
Above you mentioned ...but they should not be used to convert Length/PercentLength to device pixels. (key point device pixels, not device independent pixels as iOS uses).
If I understand this correctly @hshristov please close this issue and consider adding a nice section into migration guide (or I can take the above clarifications and submit a PR to add) - just wanna make sure I understand this 100%. 馃憤
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
Agreed. Per "works like the browser" logic, you should be able to get the user defined value (as a string?) at least by accessing the style property.
element.style.widthshould return whatever the user configured (i.e. "auto" or "150px" or "150" or whatever). I think this is what you'd get with JavaScript in the browser.And if you want the actual, runtime values, you'd use the
.effectiveWidth/Heightproperty. We just need to document when in the View lifecycle the.effectiveWidth/Heightproperty should be available (when do the layout/measure passes complete?).