Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-5818] Ti API: View proxies should behave like real JS objects - or they should be wrapped

GitHub Issuen/a
TypeNew Feature
PriorityMedium
StatusOpen
ResolutionUnresolved
Affected Version/sRelease 1.7.3
Fix Version/sn/a
ComponentsTiAPI
Labelsproxy, ui, view, viewproxy
ReporterErik Bryn
AssigneeEric Merriman
Created2011-10-20T12:09:41.000+0000
Updated2019-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.

Comments

  1. Kevin Whinnery 2011-10-20

    Agreed - right now it is discouraged to add functionality to proxy objects, but the below is perfectly valid JavaScript:
       var view = Ti.UI.createView({height:50});
       view.foo = {};
       view.foo.bar = 'hello';
       view.foo.bar = 'goodbye';
       
    Proxy objects ideally should respect the rules of JavaScript. But if they do not, they should be wrapped in a JavaScript API we support.
  2. Blain Hamon 2011-10-21

    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.
  3. Kevin Whinnery 2011-11-02

    @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.
  4. Ivan Skugor 2011-11-02

    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:
       function View(options) {
         this.Element = Ti.UI.createView(options);
       }
       
       View.prototype.add = function(TiObject) {
         this.Element.add(TiObject);
       };
       
       
       var myView = new View({ height: 50 });
       myView.add(Ti.UI.createButton({ title: 'Click me' });
       
       //now you can do this without a problem
       myView.foo = {};
       myView.foo.bar = 'hello';
       myView.foo.bar = 'goodbye';
       
    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).
  5. Jeff Haynie 2013-02-26

    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.
  6. Jeff Haynie 2013-02-26

    can someone provide a very simple test case at the issue?
  7. Kevin Whinnery 2013-02-26

    @Jeff https://gist.github.com/kwhinnery/5040513
     
       var w = Ti.UI.createWindow({
       	backgroundColor:'#fff'
       });
       
       var l = Ti.UI.createLabel({
       	text:'click me',
       	font: {
       		fontWeight:'bold',
       		fontSize:24
       	}
       });
       w.add(l);
       
       l.addEventListener('click', function(e) {
       	l.text = 'clicked!'; // yep!
       	l.font.fontSize = 12; // nope.
       });
       
       w.open();
       
  8. Jeff Haynie 2013-02-26

    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).
  9. Kevin Whinnery 2013-02-26

    On the plus side, this used to not work, but does now in 3.0:
       var db = Ti.Database.open('test');
       
       db.execute('CREATE TABLE IF NOT EXISTS test(id INTEGER PRIMARY KEY, foo TEXT);');
       
       var args = [
       	'INSERT INTO test(foo) VALUES (?)',
       	'bar'
       ];
       
       // This used to not work, but does in 3.0 for iOS!
       db.execute.apply(db, args);
       
       Ti.API.info('looping...');
       var results = db.execute('SELECT * FROM test;');
       
       while(results.isValidRow()) {
       	Ti.API.info(results.fieldByName('foo'));
       	results.next();
       }
       
       results.close();
       db.close();
       
  10. Bryan Hughes 2013-02-27

    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.
  11. Bryan Hughes 2013-02-27

    Oh and also, if we do this right, we can do things like
        var button = Ti.UI.createButton();
        console.log(button instanceof Ti.UI.Button); // logs "true"
        
    and
        function myControl(){};
        myControl.prototype = new Ti.UI.Button();
        // Add custom control stuff
        
  12. me@gmail.com 2013-02-28

    We also need to be able to do the following (based on syntax of ticket TIMOB-12595):
        var view = new Ti.UI.View();
        
        view.add = function(arguments) {
            alert('test');
        
            Ti.UI.View.add.apply(this, arguments);
        }
        
  13. me@gmail.com 2013-02-28

    Too, this should not cause performance issues:
        var w = Ti.UI.createWindow({
            backgroundColor:'#fff'
        });
         
        var l = Ti.UI.createLabel({
            text:'click me',
            customVarName: 'bla bla bla',
            font: {
                fontWeight:'bold',
                fontSize:24
            }
        });
        w.add(l);
         
        w.open();
        
    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
  14. Jeff Haynie 2013-02-28

    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()
  15. Jeff Haynie 2013-02-28

    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..
  16. me@gmail.com 2013-02-28

    @Jeff That's fine, the ability to extend/replace methods and do instance of is far more important anyway
  17. Erik Bryn 2013-02-28

    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.
  18. me@gmail.com 2013-02-28

    @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...
  19. Jeff Haynie 2013-02-28

    @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.
  20. me@gmail.com 2013-02-28

    @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
  21. Tony Lukasavage 2013-02-28

    @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.
  22. Jeff Haynie 2013-02-28

    @Tony - are we suggesting that you could either instantiate -or- use factory pattern?
  23. me@gmail.com 2013-02-28

    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.
  24. Tony Lukasavage 2013-02-28

    @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.
  25. Jeff Haynie 2013-02-28

    @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.
  26. Jeff Haynie 2013-02-28

    @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).
  27. me@gmail.com 2013-02-28

    @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
  28. Tony Lukasavage 2013-02-28

    @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:
        var controller = Alloy.createController('someController', args);
        
    is the same as
        var controller = new (require('alloy/controllers/someController'))(args);
        
        // or more verbosely, but easier to read
        var Controller = require('alloy/controllers/someController');
        var controller = new Controller(args);
        
    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.
  29. Bryan Hughes 2013-02-28

    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
        function foo() {
            // constructor stuff
        }
        var x = new foo();
        
    is more or less evaluated as this
        var foo = function(){
            // constructor stuff
            return this;
        }.bind({});
        var x = foo();
        
    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
  30. Bryan Hughes 2013-02-28

    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.
  31. Russell Frank 2013-02-28

    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 (or null in ES5 strict mode). So it's pretty easy to modify those constructors such that they can be used with or without new. 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-level unhandledException 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.
  32. Jeff Haynie 2013-02-28

    @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.
  33. Bryan Hughes 2013-02-28

    There is one caveat with checking this: the user can always do call, apply, or bind, which changes this.
  34. Kevin Whinnery 2013-02-28

    FWIW this is an alternate mechanism if the API is defined in JS (which is a good idea): https://gist.github.com/kwhinnery/5059196
  35. Bryan Hughes 2013-02-28

    @Kevin That looks a little familiar https://github.com/appcelerator/titanium_mobile/blob/master/mobileweb/titanium/Ti/UI.js#L49 :)
  36. me@gmail.com 2013-03-01

    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.
  37. me@gmail.com 2013-04-06

    As something to consider here.. this works on android (at least as of 3.1.0 preview) but not iOS:
        var win = Ti.UI.createWindow({
            open: function() {
                alert('test');
            }
        });
        win.open(); // an alert is opened with test displayed on Android, on iOS nothing happens
        
        {code)
        
        However this still does not mean it supports prototype as:
        
        
    win.protoype; ``` is undefined
  38. Ivan Skugor 2013-04-08

  39. Neeraj Gupta 2013-04-08

  40. Ivan Skugor 2013-04-08

    Thanks :)
  41. Pier Paolo Ramon 2015-01-29

    Another interesting issue related to this topic:
        
        var obj = {};
        var label = Ti.UI.createLabel({ text: 'Some text' });
        
        printPropertyDescriptor(label, 'text');
        printPropertyDescriptor(label, 'idontexist');
        printPropertyDescriptor(obj, 'idontexist');
        
        function printPropertyDescriptor(obj, name) {
        	var descriptor = Object.getOwnPropertyDescriptor(obj, 'clearTimeout');
        	var formatted = JSON.stringify(descriptor, null, 2);
        	console.log(name + " = " + formatted);
        }
        
    prints:
        [INFO] :   text = {
        [INFO] :     "writable": false,
        [INFO] :     "enumerable": false,
        [INFO] :     "configurable": true
        [INFO] :   }
        [INFO] :   idontexist = {
        [INFO] :     "writable": false,
        [INFO] :     "enumerable": false,
        [INFO] :     "configurable": true
        [INFO] :   }
        [INFO] :   idontexist = undefined
        
    but the expected result is
        [INFO] :   text = {
        [INFO] :     "writable": false,
        [INFO] :     "enumerable": false,
        [INFO] :     "configurable": true,
        [INFO] :     "value": 'Some text',
        [INFO] :   }
        [INFO] :   idontexist = undefined
        [INFO] :   idontexist = undefined
        
    So Object.getOwnPropertyDescriptor for proxies returns *always* writable:false, enumerable:false, configurable:true. Funny.

JSON Source