skariel / webalchemy

Modern web development with Python
MIT License
346 stars 21 forks source link

Use functions on client side outside events #140

Open joaoventura opened 10 years ago

joaoventura commented 10 years ago

Suppose I want to implement a function on the client side, which is not specifically triggered by an event. Something like this:

class HellowWorldApp:
    def initialize(self, **kwargs):
        self.rdoc = kwargs['remote_document']
        self.rdoc.body.element(h1='Hello World!!!').events.add(click=self.clicked, translate=True)

    def clicked(self):
        self.heavy_clientside_computation()

    def heavy_clientside_computation(self): 
        # Do something heavy on the client's browser

So, by clicking on "Hello World" the "clicked" event is fired, but then the browser throws a JS Error because "heavy_clientside_computation(self)" was not compiled to Javascript and sent to the client..

Is there a solution for this now as the framework is?

If not, may I suggest something like a decorator so the framework knows which functions are to be sent to the client? It could be something like the following, and it could be applied to all "remote" methods, including those triggered by events (as python zen's explicit is better than implicit):

class HellowWorldApp:
    def initialize(self, **kwargs):
        self.rdoc = kwargs['remote_document']
        self.rdoc.body.element(h1='Hello World!!!').events.add(click=self.clicked, translate=True)

    @remotemethod
    def clicked(self):
        self.heavy_clientside_computation()

    @remotemethod        
    def heavy_clientside_computation(self): 
        # Do something heavy on the client's browser

In this case, all function calls inside @remotemethod's would be local browser calls, unless there was the rpc() call, which would be targeting the server.

skariel commented 10 years ago

You have to translate heavy_clientside_computation like this:

class HellowWorldApp:
    def initialize(self, **kwargs):
        self.rdoc = kwargs['remote_document']
        self.rdoc.body.element(h1='Hello World!!!').events.add(click=self.clicked, translate=True)
        self.rdoc.translate(self.heavy_clientside_computation)

    def clicked(self):
        heavy_clientside_computation()

    def heavy_clientside_computation(self): 
        # Do something heavy on the client's browser

see below the hello world example, I modified it so every click on the h1 also prints to the console. I've tested it works as expected:

from webalchemy import server

class HellowWorldApp:
    def initialize(self, **kwargs):
        self.rdoc = kwargs['remote_document']
        self.rdoc.translate(self.print_something)
        self.rdoc.body.element(h1='Hello World!!!').events.add(click=self.clicked, translate=True)
        self.rdoc.body.element(h2='--------------')
        self.rdoc.stylesheet.rule('h1').style(
            color='#FF0000',
            marginLeft='75px',
            marginTop='75px',
            background='#00FF00'
        )

    def print_something(self):
        print('hi there!')

    def clicked(self):
        self.textContent = self.textContent[1:]
        rpc(self.handle_click_on_backend, 'some message', 'just so you see how to pass paramaters')
        print_something()

    def handle_click_on_backend(self, sender_id, m1, m2):
        self.rdoc.body.element(h1=m1+m2)

if __name__ == '__main__':
    # this import is necessary because of the live editing. Everything else works OK without it
    from hello_world_example import HellowWorldApp
    server.run(HellowWorldApp)

I think the decorator is a good idea but I need to think how this could be implemented, since currently it needs the runtime remote-document obejct to work...

EDIT: no self. needed to call heavy_clientside_computation

joaoventura commented 10 years ago

I also saw that "problem" with the remote-document object. The only solution I could find so far was to make a base class for the framework (something like WebAlchemyApp - which everyone's apps would inherit from), and make the remote-document a property of that base class. Then, the decorator would only be a wrapper to "self.rdoc.translate(func)"..

What do you think? Any drawbacks by having a WebAlchemyApp Base Class? This would allow to simplify things such as this one..

skariel commented 10 years ago

I'm not sure how this would work... Decorating happens at "compile" time right? I'm ok with having to inherit, could you open a pull request on a different branch so I can test, Or just put the needed code in this thread? Otherwise you'll have to explain in greater detail how it will work so I'll understand and implement

Thanks!

skariel commented 10 years ago

Maybe one way to make this work is having a decorator that just "marks" which functions should be translated. Then when the app is created the server could scan the app and translate the required functions

joaoventura commented 10 years ago

I'm also not very used to decorators, but I'll try to get something to work.

Your second idea (translation on-the-fly) is also good since it would allow to do another thing that I was going to suggest later: Apply the same thing (remotemethods vs local methods) on generic classes. This could be useful for API's where you would like much of the code of a class to be executed on the client, and some code on the server (things like DB accesses).

Things like the following would be great to do transparently:

class SomeAPI:

    def db(client, op):
        """ This opens a connection on the local drive and executes an operation. """"
        conn = open_connection("home/user/path/somedb.db")
        if (op == "save"):
            conn.save(client)
        elif ......

    @remotemethod
    def saveClient(self, client):
        self.db(client, "save")

The remote method would "see" that the function "db" was not local, and would in the background do an RPC call to execute it on server...

To put code on the client allows us, developers, to have less server requirements, that is, we can use cheaper servers.. :)

Iftahh commented 10 years ago

Just a small note: I think a better name for the decorator would be @clientside.

remote method is an unclear name, If you have server centric point of view you would read remote as client side and if you have browser centric point of view you would read remote as server side.

On Wednesday, January 29, 2014, João Ventura notifications@github.com wrote:

I'm also not very used to decorators, but I'll try to get something to work.

Your second idea (translation on-the-fly) is also good since it would allow to do another thing that I was going to suggest later: Apply the same thing (remotemethods vs local methods) on generic classes. This could be useful for API's where you would like much of the code of a class to be executed on the client, and some code on the server (things like DB accesses).

Things like the following would be great to do transparently:

class SomeAPI:

def db(client, op):
    """ This opens a connection on the local drive and executes an operation. """"
    conn = open_connection("home/user/path/somedb.db")
    if (op == "save"):
        conn.save(client)
    elif ......

@remotemethod
def saveClient(self, client):
    self.db(client, "save")

The remote method would "see" that the function "db" was not local, and would in the background do an RPC call to execute it on server...

To put code on the client allows us, developers, to have less server requirements, that is, we can use cheaper servers.. :)

Reply to this email directly or view it on GitHubhttps://github.com/skariel/webalchemy/issues/140#issuecomment-33619111 .

skariel commented 10 years ago

just tried so close this issue, but everything I try has some major downside. For now I think it's better to just continue with what we have.

Also- the way I showed above for translation is dangerous. It can create namespace problems. For e.g. when using translate('print_hi') it will create a function named print_hi in the global js namespace. Imagine you translate click- you would have 10 versions.

The solution is to use self.print_hi = translate('self.print_hi') and then call this function from JS with srv: srv(self.print_hi)(). This works since the proxy has a mangled name, and the srv escapes that name.

So the correct hello world above should look like:

from webalchemy import server
from webalchemy.remotedocument import rpc, srv

class HellowWorldApp:
    def initialize(self, **kwargs):
        self.rdoc = kwargs['remote_document']
        self.print_hi = self.rdoc.translate(self.print_hi)
        self.rdoc.body.element(h1='Hello World!').events.add(click=self.clicked, translate=True)
        self.rdoc.body.element(h2='--------------')
        self.rdoc.stylesheet.rule('h1').style(
            color='#FF0000',
            marginLeft='75px',
            marginTop='75px',
            background='#00FF00'
        )

    def clicked(self):
        self.textContent = self.textContent[1:]
        rpc(self.handle_click_on_backend, 'some message', 'just so you see how to pass paramaters')
        srv(self.print_hi)()

    def print_hi(self):
        print('hi!')

    def handle_click_on_backend(self, sender_id, m1, m2):
        self.rdoc.body.element(h1=m1+m2)

This version works, I just tested. (note the imported srv and rpc functions above are just empty methods so the IDE doesn;t complain, I still haven't syncd this with github...)