[TIMOB-5818] Ti API: View proxies should behave like real JS objects - or they should be wrapped
GitHub Issue | n/a |
---|---|
Type | New Feature |
Priority | Medium |
Status | Open |
Resolution | Unresolved |
Affected Version/s | Release 1.7.3 |
Fix Version/s | n/a |
Components | TiAPI |
Labels | proxy, ui, view, viewproxy |
Reporter | Erik Bryn |
Assignee | Eric Merriman |
Created | 2011-10-20T12:09:41.000+0000 |
Updated | 2019-04-26T19:04:12.000+0000 |
Description
Component: Ti API
If view proxies aren't going to behave like real JS objects (e.g. nested properties of proxy objects are immutable), and that isn't going to change, I propose we should develop a pure-JS API that wraps the proxies.
I've begun wrapping Ti views in my [SproutCore-Titanium project](https://github.com/ebryn/sproutcore-titanium).
Let's get a discussion started.
Agreed - right now it is discouraged to add functionality to proxy objects, but the below is perfectly valid JavaScript:
Proxy objects ideally should respect the rules of JavaScript. But if they do not, they should be wrapped in a JavaScript API we support.
The issue is very nontrivial, especially because by design the proxy objects have to do things that Javascript was not designed for, such as multithreading and being accessible in multiple contexts. There are long-term designs in the works, but it requires extensive changes under and over the hood. For example, to move all the UI-reaching JS (IE, the JS that can access Ti.UI.foo) into a single thread, window.url would need to be dropped, and that does include rewriting KS, even.
@Blain I don't doubt it would be non-trivial - however, having special objects that don't conform to the laws of JavaScript has caused many bugs in projects I and PS guys have worked on. If changing the behavior of proxy objects isn't an option, we should do something in JavaScript land to shield developers from the weirdness of these not-quite-objects. A thin shim layer of some kind, perhaps.
Kevin, (if I may add) there are some attempts to solve lack of OOP features in Titanium that came from few community members. Some of them could be helpful in this case. Basically, they all are some kind of wrappers around Titanium components. One approach is to write wrappers that behave as proxies to Titanium objects. That wrappers are just native JS objects. So ... for example:
This is a basic idea and it is OK in my opinion (but it needs more work to make it work as it should). I had similar idea, but I don't use wrappers as proxies (because I wanted to solve some other problems). Anyway, I hope this idea could be useful. If you need help or more info about this or other ideas, please let me know (or ask in Q/A).
Looking at this I'm not sure why this isn't working. Blain's comment about UI etc don't seem to make any sense to me. Like Kevin said, JS objects should act like JS objects and a viewproxy at the JS level should behave like other JS objects. I'm actually surprised this isn't the case.
can someone provide a very simple test case at the issue?
@Jeff https://gist.github.com/kwhinnery/5040513
thanks, that makes total since. this should be rather easy to fix in the platform. i was confused my the immutable reference -- i think he was saying (now that i understand it) they are currently immutable, and they should be mutable (and he's correct).
On the plus side, this used to not work, but does now in 3.0:
This ticket actually dovetails quite nicely into TIMOB-12595. In fact, I would go so far as to say that they are probably dependent on each other.
Oh and also, if we do this right, we can do things like
and
We also need to be able to do the following (based on syntax of ticket TIMOB-12595):
Too, this should not cause performance issues:
Right now this is an issue because vars on a proxy object get passed to the native code, rather then having a whitelist of native properties on each object leaving the rest in JS land
I don't think we'll change the Factories into Classes that can be instantiated. We are using a Factory design pattern for Titanium and we're not going to be changing that anytime soon. So, Ti.UI.createView() will be the way to instantiate a View object, not new Ti.UI.createView()
the problem is we have to pass to native land because you might need to reference those properties (if we are trying to make them full-fledged JS objects) in other functions in native land or in modules. i think we just need to improve the performance around this..
@Jeff That's fine, the ability to extend/replace methods and do instance of is far more important anyway
Glad to see some movement around this finally. I ran into this issue with implementing bindings in views in ember-titanium. FWIW, I think the factory pattern is really ugly. I think allowing the use of the constructor functions would be great.
@Jeff unless... and im just spit balling here... what if UI elements were wrapped in JS on the module level? Ti native modules support this, it would just need to be implemented... again just a thought. All that would really be needed from an architecture standpoint would be for Titanium to implement JS Proxies like ecma 6 so that properties could be watched...
@Erik - this is a stylistic design item. some people love it, some people hate it. hard to please everyone in designing anything since mostly it's a matter of opinion vs. merit. @Matt - there may be some way to optimize this somehow, maybe using Bryan's new code processor, whereby we can be smart at when they need to cross the bridge. Maybe they could load from JSLand to nativeLand somehow "on demand" if only required and not passed to native bridge unless that is required. That might be something useful and not too hard to figure out.
@Jeff That would be great, in particular reducing the need to save properties like Ti.UI.SIZE to a var in JS land to prevent crossing the bridge with each use
@Erik and @Jeff - Aside from the stylistic standpoint, forgetting a "new" is incredibly easy for developers to do and equally as difficult to identify based on runtime errors of which a developer may provide insufficient details.
@Tony - are we suggesting that you could either instantiate -or- use factory pattern?
If the UI were wrapped in JS as I suggested above... devs who like "new" better (such as myself) could easily add that syntax should they so choose. Plus, from a code structure standpoint, I should be able to extend a view to create a custom view... and for this I need a constructer method to do correctly.
@jeff - defending the factory pattern in that it can avoid dangers in code that are unique to languages like Javascript which will silently move on when a constructor is invoked in the absence of a "new". That's part of the reason Alloy has the "create" functions as well. Personally, I'm a long time strongly-typed OOP guy, so I prefer the "new" syntax. That said, I think the factory pattern better suits the mass usage of Titanium and its developers.
@Tony, @Matt - as long as we could support both styles, I would be fine with the change. The actual change to support a constructor in native is pretty ... we just can't break the API.
@Tony - Factory pattern is "OO Design" http://en.wikipedia.org/wiki/Factory_method_pattern -- especially useful when you're returning objects from an interface of an unknown type (for example, depends on the underlying platform object implementation). However, I can understand why devs also like "new" so they can create objects as well. I could go either way. If we can do both, i would be fine. The challenge will then be: when do you use which? (it could be a style thing, and maybe i'm OK with that).
@Jeff I 100% agree... it would suck to have that much of a breaking change. Even I, a fan of the constructer, would disagree with removing it all together (at least at first...) My suggestion would allow for a more graceful removal over time, if removed at all
@Jeff - haha, i guess citing OOP as my background was a poor way to phrase. I was referring more to the idea of manual class instantiation, personally preferring a constructor rather than a factory function. Alloy actually supports both now for its operations:
is the same as
Both are perfectly valid code to see in a controller. The latter syntax is a little harder for devs to grasp since the generated modules are actually constructors, that is, the
module.exports
_is_ the constructor. It does however give me more confidence as a developer that I have total control of the instance. While Alloy.createController() does no additional operations to the instance, it's a layer of separation between me and the generated instance. The former syntax is a bit more clear and does not force developers to understand the underlying folder structure if they don't want. But perhaps I'm just clouding the issue by mentioning Alloy as it is not subject to the same issues as are Titanium proxy objects.One thing to keep in mind is that constructors in JavaScript are not special. They are just a function call with an implicit bind() and an implicit "return this". Basically this
is more or less evaluated as this
Basically what I'm saying is that the instantiation patter is very similar to the factory pattern in JavaScript, even if they don't appear that way on the surface
Also, I don't think the factory pattern is especially useful in dynamically typed languages. The whole idea is you don't know what you are going to create, or who will create it, but really the factory pattern was introduced to get around the limitations in strongly typed languages where the full type chain has to be known in advance. This is a non-issue though in a dynamically typed language like JavaScript, so the usefulness of factory methods is reduced.
If I may - @Tony, it's fairly easy to detect if
new
was used on a constructor. If it wasn't,this
will be the global object (ornull
in ES5 strict mode). So it's pretty easy to modify those constructors such that they can be used with or withoutnew
. Really, the native-binding-proxies should *never be exposed in the first place*. ES5 is powerful enough these days that simple JS proxies could be created that interface with the native-binding-proxies. In fact, they can be generated based on the JSCA (I think Matt did something like this already), and this is relatively easy to do. Then, you can do all sorts of fun things, like type checks on arguments, instanceof checks, and an app-levelunhandledException
event rather than that ugly "Runtime Exception" screen (by wrapping all added emitters). This can also reduce trips across the bridge. This is how NodeJS manages native bindings, and it makes a lot of sense; there's way more flexibility on the JS end, and we don't have to deal with strange native bindings that don't act like real JS objects.@russ- this is how android/V8 basically works today. there's always better ways of doing something after you've done it and we'll improve over time.
There is one caveat with checking
this
: the user can always docall
,apply
, orbind
, which changes this.FWIW this is an alternate mechanism if the API is defined in JS (which is a good idea): https://gist.github.com/kwhinnery/5059196
@Kevin That looks a little familiar https://github.com/appcelerator/titanium_mobile/blob/master/mobileweb/titanium/Ti/UI.js#L49 :)
Honestly me and my team would be willing to do this... but I think the desire here would also be to rework the code to be more modular, thus outside the scope of what my team has the time to do.
As something to consider here.. this works on android (at least as of 3.1.0 preview) but not iOS:
win.protoype; ``` is undefined
Thanks :)
Another interesting issue related to this topic:
prints:
but the expected result is
So
Object.getOwnPropertyDescriptor
for proxies returns *always*writable:false, enumerable:false, configurable:true
. Funny.