User Manual

License

yaji is written by Josef Hahn under the terms of the AGPLv3.

Please read the LICENSE file from the package and the Dependencies section for included third-party stuff.

About

Yaji is a Python package allowing to implement graphical user interfaces that are accessible by just web browsers. This includes usual desktop or mobile web browsers, even on remote machines, but also embedded desktop applications.

Up-to-date?

Are you currently reading from another source than the homepage? Are you in doubt if that place is up-to-date? If yes, you should visit https://pseudopolis.eu/wiki/pino/projs/yaji and check that. You are currently reading the manual for version 0.1.463.

Maturity

yaji is in production-stable state.

Dependencies

There are external parts that are used by yaji. Many thanks to the projects and all participants.

icon_package PyQt5 incl. WebEngine, optional : otherwise you will need a usual web browser

icon_artwork banner image, included : _meta/background.png; license CC BY 2.0; copied from here.

Overview

Hello World

class MyApplication(yaji.Application):

    def __init__(self):
        super().__init__()
        self.setbodyleft(yaji.View(label="Hallo Welt."))

Yaji is a library for graphical user interfaces that are accessible by web browsers. However, it is not a framework for websites. It would lack some features for that on the one hand, while it has some features that would not make sense for that on the other hand. Instead, the entire web server comes up when an end user starts a Yaji-enabled application. That application steers one single user interface, as a usual program with a local user interface would do. The web server is bound to that particular process with all that user interface state, which does not make it a good choice for a website.

Compared to a local user interface, it is a bit more tricky to implement, but it allows to access the user interface easily via the network. It might be not worth taking the additional effort, if this network capability is not a requirement for you.

Writing browser side code is possible, but not needed. Yaji allows implementing rich user interfaces by just Python code. It also allows direct access on browser side for advanced cases, e.g. by additional JavaScript code, but a good practice is to use that in a minimal way (usually not at all).

How applications are shown

By default, a Yaji user interface tries to open a window on your local desktop for your user interface on start. This follows the assumption that local access is the preferred way.

Internally, it uses the WebEngine in PyQt for showing a window that mostly is a tiny browser, just without all the controls around the actual website. If that fails, it tries to open itself in the local browsers.

Furthermore, if it succeeds or not, there is a tiny local web server, which usual web browsers can connect to. Of course, depending on network structure and firewall rules, this can also be a web browser on another machine. It is possible to connect to the user interface with multiple browsers in parallel, even though this is only useful in a few scenarios. Depending on how it is set up by the application around it, it may or may not be possible to disconnect from it completely with all browsers, and to reconnect back much later without loss of state data.

Note

Setting the environment variable YAJI_CALLCMD to a custom command line causes a Yaji application to open new browser views by executing that instead of its default routine. This command line should contain %u for taking the URL to open.

Views run decoupled from application code

There is one challenge with this approach by nature that leads to the additional trickyness in comparison to a classic local user interface. In the local case, there is one “UI” thread with a main loop, which all user interface code gets piped through in a mostly first-in-first-out way. This makes it easy to keep things in a consistent state: Whenever any interaction by the user occurs, program code can directly compute a new user interface state, like showing/hiding or enabling/disabling some parts of the user interface depending on some conditions. That typically happens in no time the user could realize. The user is also not able to intervent meanwhile nevertheless, even if computation takes longer, since all further user interactions just take a place in the queue of the main loop until the computation finishes. For example, there is no way to click on a button while a computation is going on that eventually disables it (unless e.g. some multi-threaded application code is explicitly doing it this way).

A Yaji application has to implement such situations differently, because that single main loop does not exist. The application (Python) code and the presentation of the user interface run in a much less encoupled way. The user interface will typically not block while application code is running. Furthermore, application code that modifies the user interface will potentially finish before the view actually reflects that (which is quite understandable because there can be from no to n views, e.g. by opening it in multiple browser tabs). This makes it harder to keep the user interface in a consistent and ‘correct’ state (whatever that means for a particular user interface logic). For example, a user could interact in some way that triggers the application code to disable a particular button, but then click on that button before it actually becomes disables. In fact, there are two asynchronous transitions in this case: The user interaction triggering the application code and the application code triggering the button state change. There are some ways to work around that, like curtains, or implementing some pieces of JavaScript code, which will be mentioned in detail later. All of them come with some additional efforts to spend.

Sample applications

For many aspects of Yaji there are some small sample applications. The following text will explicitly refer to some of them, but it is a good general advice to study them for better understanding. However, there are some remarks one should keep in mind while reading them:

  • They tend to keep things simple and are generally not production ready,

  • they typically do completely useless stuff, just for showing a particular feature or aspect of Yaji, and

  • they sometimes solve a problem in a ‘creative’ way, while one would solve it in completely different ways for real code,

  • they sometimes play around with different ways of doing the same thing (e.g. writing a data structure in different ways) if that is worth showing.

  • However, many of them show only a subset of the available functionality.

The sample applications are located in ./_meta/sampleapps. Each .py file is for one sample application (but ___sampleapp.py which has a special internal meaning). They are directly executable from command line. Some of them come with additional files, e.g. a browser side JavaScript, which are important parts of the application. For a foo.py this would be __foo.js. They are automatically loaded by Yaji without an explicit reference to them somewhere in the .py code.

A typical Yaji application

A typical Yaji application potentially does the same things as local graphical applications do. It implements a user interface based on Yaji typically in the following way (just trying to give an idea about many different things; it is not required to understand each point in detail now):

  • It implements a subclass of yaji.app.Application. This can either be a real application, or just another abstract enhancement for implementing by deeper subclasses. The following assumes a full, non-abstract implementation.

    • Inside the constructor, it configures the user interface by assembling complete forms from basic parts like buttons, labels and layouts, and assigning that to the main view.

      • Those views are connected to the application code by data bindings and event bindings. The former keep data on view side in sync with some data on application code side, while the latter trigger some code execution when some event occurs in the view (e.g. the user triggered a button).

      • Application code only sets completely assembled views and cannot change only some properties of some subelements or insert/remove/replace subelements of a view; with some important remarks:

        • Data bindings are an exception to that rule, since keeping a property of a widget in sync with application data is exactly their primary duty. Furthermore, since visibility and enabled are also just usual properties of each widget, many kinds of dynamic changes in the user interface structure can be realized by data bindings to them.

        • The usual main view is split into a left and a right column, so there are at least two separate views one can set independently from the other. There are also some more situations that involve building a view, like dialogs.

        • Setting the main views to new ones at some times during the application execution is possible, so structural changes of the user interface during application execution are possible by completely rebuilding a view each time.

    • It provides request handlers for processing all kinds of requests by the view side. For now, those are the handlers used in the event bindings (see above), but they can have other uses. So they react on some kinds of user interactions like triggered buttons or menu items (while data bindings are only internally related to that).

      • A request handler is a method of your subclass, usually named in a particular way, and annotated in a particular way. It contains the application specific handler code for that event.

    • It optionally overrides some methods in order to customize some low-level behavior, e.g. as one piece of the integration into something bigger.

  • It optionally provides some browser side resources for advanced cases, e.g. a .css style file or a .js JavaScript. If there is a file like __foo.js for an application foo.py, Yaji will automatically load it without any code needed.

    • Browser side code can implement more complex and more tailor-made widget classes that can be used by the application code instead of simpler widgets like buttons and labels. They can react to the user in a more direct way than application code can, so this is needed in some cases.

    • It can do everything user interface related that application code can do, and much more, in a more direct way, meaning: next to the user, but far away from the application code side instead.

    • It has access to data sources, but as mentioned earlier, there is will be a time gap between updates on application code side and browser side.

    • Communication with the application code side is based on Ajax calls and/or higher-level mechanisms based on that.

The following chapters will go more into detail.

Application starting and stopping

At some time typically during startup, the application code creates an instance of that subclass and calls yaji.app.Application.start() on it. Eventually, there should be some code somewhere that triggers to stop the application again. A later chapter will go more into details, but the following code is one example of doing both:

from yaji import *  # do not use * in your code


class MyApplication(Application):

    def __init__(self):
        super().__init__()
        self.setbodyleft(View('Button', label="Stop",
                              OnClicked=self.bindevent(BackendFunction('stop'))))

    @RequestHandler.for_urls(auto=True)
    def _do_stop(self):
        self.stop()


if __name__ == '__main__':
    MyApplication().start()

Correctly importing it

There is nothing one can particularly do wrong with importing yaji. However, there are different ways, of doing it, and mixing them can confuse at first. This documentation does mix it occassionally for different reasons, so you should be aware of it.

The yaji package is divided into some submodules, each of them having some direct members, like class or function definitions. The main application base class is Application inside the app submodule. So, after import yaji.app its qualified name is yaji.app.Application. The root package itself also includes all members that are typically used by outside code. This is for convenience as it reduces the amount of typing and/or thinking about the correct imports to make, but it can be confusing when seeing Yaji class or function names. After just import yaji the same class has the qualified name yaji.Application. Due to technical reasons it has only this name and not the longer one at all (of course it is possible to make both import statements if that is needed e.g. during some code refactorings).

So, whenever a short name is mentioned somewhere, there is also a longer name (which also includes the submodule name) that is pointing to the same thing. The API reference here lists all module members with their long names, so one way to find out a long name is to search for the very last segment of the name in the API reference.

For keeping lines shorter, most examples in this text assume from yaji import ... and would just refer to e.g. Application.

Basic user interfaces

setbodyleft and setbodyright

Application code sets up the user interface inside the constructor and optionally any time later, by building complex user interface structures from basic ones like buttons or labels, and layout elements, which align widgets (or other layout elements) inside it in a particular way. It applies that by calling yaji.app.Application.setbodyleft() and/or yaji.app.Application.setbodyright(), like the following:

def __init__(self):
    ...
    self.setbodyleft(View('Label', label="Foo"))

This shows a widget of class Label with some properties, precisely a label showing the text “Foo”. As the method name suggests, there is a distinction between left and right. This does not mean anything as long as either only setbodyleft or setbodyright is used. When both are set in parallel, the view splits into a left and a right side. There is a splitter dividing both sides, which the user can move.

View specifications

The parameters passed to yaji.app.Application.setbodyleft() and some other functions are instances of yaji.gui.View. They completely specify a user interface view with simple elements like buttons, and stack them together in some potentially complex ways with layouts. Each subpart within this structure, i.e. each simple elements as well as each layout is a View, up to the final complete specification. Each View instance has a class, which is a string like 'Button', 'Label' or 'HorizontalStack' and some property assignments. Those assignments can be just any values of types that make sense (e.g. strings for label texts). They can also be data bindings, and for events, they are usually event bindings. The following example shows another view configuration, still without layouts, and kind of nonsensical:

View('Button', label=self.binddata('buttonlabeldatasource'), enabled=False,
     OnClicked=self.bindevent(BackendFunction('dosomething')))

Data bindings and event bindings are explained in detail later.

There is a shortcut for label, which you could find used in other applications like the samples. Whenever a label property is given but no class, "Label" class is used. So, the following lines each specify the same thing:

View('Label', label="Foo")
View(label="Foo")

Layouts

Building complex views out of simple pieces stacked together uses the same mechanism, with e.g. a 'HorizontalStack' class widget with one of its properties used for assigning children widgets. However, for builtin layouts, there is a slightly different, shorter notation, since there are special classes available for them. The following example stacks some pieces together:

self.setbodyleft(
    VerticalStack(
        View('Label', label="Foo"),
        HorizontalStack(
            View('Label', label="Bar"),
            View('Label', label="Baz")
        )
    )
)

Next to yaji.gui.HorizontalStack and yaji.gui.VerticalStack there are also some more layout. The yaji.gui.Grid layout is a quite flexible and powerful one:

Grid(
    View('Label', label="Foo", row=0, col=0),
    View('Label', label="Bar", row=1, col=0),
    View('Label', label="Baz", row=1, col=1)
)

Scroll views

Another kind of container, similar to a layout, is yaji.gui.ScrollView. It is used whenever a subelement inside the user interface might be larger than the space available for it. It does so by scroll logic and can be used like this:

ScrollView(
    View('SomethingLarge')
)

Note

Having more than one widget inside a scroll view requires to place a layout around them before.

There are also some other, more complex containers like tab views or carousels. A later chapter helps to find out details about them. However, there are no shortcuts available for them, so using them requires a complete View specification with a class name and some property assignments.

Sizing

Due to the size the view window has and due to the user interface specification assembled from hierarchies of layouts and actual widgets, sizing for the inner leaf widgets is predetermined to some degree. However, the view window typically does not have exactly the perfect size for presenting the specified user interface. It might be too small in one or both dimensions. That would break the usability of the interface and could be considered a defect. Scroll views should be used in order to avoid that. Whenever the view is larger than necessary, the additional space is distributed to the widgets. This happens while considering a stretch factor on each widget, which allows to influence how the distribution takes place. Specify the optional hstretch and/or vstretch properties for that, like this:

HorizontalStack(
    View('Label', label="Foo", hstretch=2),
    View('Label', label="Bar", hstretch=5),
    # we can also override the auto sizing (but still stretching)
    View('Label', label="Baz", hstretch=1, width='30pt')
)

Since each layout behaves like a widget (in fact is a widget), stretch factors can also be assigned to them. This can be crucial for getting the desired result.

In some pieces of code one might find the usage of those stretch factors by a different name: horizontalStretchAffinity and verticalStretchAffinity do exactly the same with a more verbose name.

Layouts do not only stretch widgets, but they might also align them on the other axis by giving them the same height or width. The properties strictHorizontalSizing and strictVerticalSizing can be set to True for making a widget ultimately refuse any sizes larger that its native one even for alignment purposes.

Configuring the main view itself

After the introduction about how to specify a user interface body, this chapter shows some ways to configure the outer frame of that.

Header text

The application view has an optional first and second header text. Both are shown in the header of the main view. Set them like this:

class MyApplication(Application):

    def __init__(self):
        super().__init__()
        self.sethead1("My Application")
        self.sethead2("Foobar Baz")

Icons

The application icon can be specified like this:

class MyApplication(Application):

    def __init__(self):
        super().__init__()
        self.add_staticfile_location("/my/static/files/directory")
        self.icon = "someimage.png"

Finding more information in the API references

This documentation text introduces each larger block of functionality in some depth, but it might not provide all details and all possibilities for each one; in fact it might not even mention all detail aspects of the common functionality, like listing all available widget classes. The same is also true for the sample applications. This kind of information are provided by the API references instead.

Later in this text, there is an API reference for the application code (i.e. backend) side, which lists the Python side of the Yaji API.

Clove

The foundation and implementation of widgets and user interfaces on the browser side is the Clove library. It implements what a widget is, how they play together, and what widget classes exist. There is a dedicated Clove documentation, which lists all that, including descriptions of all those widget classes and what properties they have.

It is located in https://pseudopolis.eu/wiki/pino/projs/clove/FULLREADME/FULLREADME.html.

Note

There is a common naming scheme in all APIs related to Yaji regarding non-public members. Unless there is a better way, they at least have a name starting with “_”. Some of them are listed in the API references for special purposes, although they are non-public. In most cases it is okay to ignore them.

Data sources and data bindings

Data sources are a high-level mechanism for data exchange (potentially) between application code side and browser side. It is the primary mechanism for showing non-static data in a user interface.

A data source is a special object (i.e. an instance of some particular classes) that keeps any kind of data and that can be used for a data binding somewhere inside a view specification. The data binding will lead to a connection between the data source and some property in that view specification, which keeps them in sync. This can be used in both directions: It updates the bound widget property with new data when the data source content changes, and it updates the data source content when the widget property changes (e.g. when the user typed some text in a field).

The following shows a simple example for a data source usage:

class MyApplication(Application):

    def __init__(self):
        super().__init__()
        ds = self.create_datastore()
        self.setbodyleft(View('EditBox', text=self.binddata(ds)))

See also yaji.app.Application.create_datastore() and yaji.app.Application.binddata(). The variable ds is a yaji.datastore.DataStore object. It can be used to set or get the text content of that EditBox in this case.

Note

It is possible to use one data source in more than one data binding.

Server and local data sources

There are two different types of data sources. A yaji.datastore.DataStore as created in the example above comes with an actual data storage on application code side. Data bindings will keep this storage in sync with the user interface state, so application code can read from it and change it. Another type is yaji.guibase.BrowserSideDatasource. Those data sources can be created by yaji.app.Application.create_clientlocal_datastore() instead, and used in the same way as in the example above.

The latter type of data sources (also called a local data source, contrary to a server data source) are not connected to the application code side. They will not exchange any data between multiple browser windows presenting the same application either. Each view (i.e. each browser window showing the application) has an own, isolated storage for it. Due to that, a single binding like in the example above has limited usage, but having more than one binding for the same local data source might be usable.

Note: Local data sources are a tradeoff. Their obvious disadvantage is the decoupled way in which they keep their data, outside of the range of the application code. Their first duty is to help keeping the user interface in a consistent state. Server data sources need to push their data to the server and to let the server push it to all the other bindings. As mentioned earlier, all data exchange mechanisms between application code side and browser side are inherently indirect and deferred, i.e. a change on one side will eventually be reflected on the other side at a somewhat later time. A local data source reflects the other bindings in a much more direct way, which is very helpful in some situations.

Although application code is not able to make changes to local data sources, it can initialize the data source, like in the following:

with self.create_clientlocal_datastore() as ds:
    ds.setvalue("Foo")
self.setbodyleft(View('EditBox', text=self.binddata(ds)))

Data bindings

The examples above show the usage of yaji.app.Application.binddata() for binding an existing datasource to some widget properties. There is also a slightly different notation, which also can be found in some of the sample applications. It uses yaji.app.Application.bindserver() and yaji.app.Application.bindlocal() instead, typically with a string argument. They do a similar thing, but with a data source name. If there already is a data source with that name, it uses that, otherwise it creates a new one (a server or local one). Note that also yaji.app.Application.create_datastore() and yaji.app.Application.create_clientlocal_datastore() allow to assign a name to the new data source.

There are some configuration switches that control the behavior of a data binding. See the parameters of yaji.app.Application.binddata().

Direction

The data direction of a binding specifies if data updates happen either only from the datasource to the widget property, or only the other way around, or in both directions. See the data_direction parameter of yaji.app.Application.binddata(), the bindings_text.py sample application, and the following example:

self.setbodyleft(View('EditBox', text=self.binddata(ds, yaji.BindDirection.ToWidget)))

Note

The default is bidirectional flow as this is the only useful thing for typical cases.

Converters

A data binding can be equipped with a converter that translates between the data representation inside the datasource and some view-specific representation. This is an advanced feature that also involves including browser side code, which is the subject of a much later chapter. See the convertername parameter of yaji.app.Application.binddata() and the bindings_converters.py sample application.

Data sources beyond bindings

There are also other usages of data sources that do not involve data bindings at all. Most of them use data sources in a much broader manner, storing more complex data structures instead of just single values in them.

A data source allows to have that structures by the following framework:

  • A data source node can store an arbitrary value, and

    • potentially has a two dimensional grid of child nodes.

  • Each data source has a root node.

A data binding only makes use of a small part of that flexibility since it only works with the root cell and no structure of child nodes. But in a broader sense, data sources can also keep data in a list, table or tree structure by that. This is used e.g. in "TreeView" widgets by directly assigning a data source to the datasource property (see the treeview.py and tableview.py sample applications) and for keeping menus (see the menus.py sample application).

Event bindings

Data bindings allow to transfer data, including a way to let application code listen for changes e.g. made by the user. But they do not help for some other kinds of user interaction. A button for example, which can be clicked by the user, is not helpful with a data binding. Instead, they provide some events that application code can bind to.

bindevent

The available events depend on the widget class, so they can be found in the Clove API reference for builtin ones. For a button, an event binding could be like this:

self.setbodyleft(View('Button', OnClicked=self.bindevent(BackendFunction('something'))))

Implementing the handler function involves adding a new method to the yaji.app.Application implementation and annotating it in a particular way. Later chapters show more details, but for the example above, a handler could be implemented like this:

class MyApplication(Application):
    ...

    @RequestHandler.for_urls(auto=True)
    def something(self):
        spectacularly_succeed(fail_on_mondays=True)

BackendFunction

A yaji.guibase.BackendFunction as used in the event binding above is a reference to a function/method on the application code side.

Later chapters will also introduce other ways to use them.

They also allow to pass some additional arguments to the function.

BrowserFunction

Similar to yaji.guibase.BackendFunction there is also yaji.guibase.BrowserFunction. They bind to a function on browser side. This is an advanced feature that typically also involves including browser side code, which is the subject of a much later chapter.

See the eventbindings.py sample application for both kinds of function references and ignore the _do_ prepending the method name for the moment.

Event data

Whenever a browser side event occurs, it comes with some event data. Its content depends on the event, and for many events there are no useful event data at all. See yaji.request.Request.uieventdata and the eventbindings.py sample application for (a particularly useless example of) how to get this data.

Request handlers

The bindevent example above makes use of a yaji.guibase.BackendFunction and defines a handler method for it, that is annotated to be a request handler. Request handlers can be called from browser side, for executing event handlers, but also for other things explained in later chapters.

See also yaji.reqhandler.RequestHandler for more details.

Note

Request handler calls do not get enqueued in a main loop, but all of them execute in parallel, in separate threads.

The _do_ prefix

The easiest way to define a handler method is to just give it the same name as referred to in the yaji.guibase.BackendFunction. There is another naming scheme however, which is recommended to use instead, that means prepending _do_ to the method name (leaving the name in the event binding untouched). The method name would then be _do_something instead.

URL mapping

URL mapping is the lookup process that finds the right request handler for the URL of a request from browser side. For the bindevent example above it would find the something (or _do_something) method for the request URL /something.

Instead of this default naming scheme, it is also possible to associate a request handler with other URL patterns. See the parameters of yaji.reqhandler.RequestHandler.for_urls() for more information. This is optional and mostly for naming aesthetics.

Downstream execution

By default, request handlers are executed while leaving the response stream open for the final response. Later chapters show how to work with such responses. For event handlers, however, responses do not have any meaning.

Whenever a request handler potentially runs for a longer time, this can be a problem. There is no precise threshold that defines ‘a longer time’, as it depends on browsers and network infrastructure, but it is safe to consider anything larger than a few seconds as long. In such cases, request handlers should be annotated as downstream ones. They directly return an empty acknowledgment to the browser and execute afterwards. Those handlers are not able to do some things, e.g. to return a response to the browser. The latter is not a restriction for event handlers anyway (in any other case, a custom indirect way to return the response has to be established).

Annotating a request handler by yaji.reqhandler.RequestHandler.run_downstream() makes a request handler a downstream one, like in the following example:

@RequestHandler.for_urls(auto=True)
@RequestHandler.run_downstream()
def something(self):
    go_on_summer_vacation()

User feedback

The user feedback subsystem provides simple ways for simple dialog based interactions with the user. Those dialogs can show a message text to the user and have some buttons or ask the user to enter a text, and some other things. See also yaji.userfeedback.UserFeedbackController, yaji.Application.userfeedback, the eventbindings.py sample application and the following example:

class MyApplication(Application):
    ...

    @RequestHandler.for_urls(auto=True)
    @RequestHandler.run_downstream()
    def _do_something(self):
        self.userfeedback.messagedialog("Hallo Wereld.")

Note

User feedback cannot be used in request handlers that are not downstream ones, because that would inevitably violate the rule of avoiding long execution times with them.

Dialogs

Dialogs are a way of interacting with the user in pseudo popups. Compared to user feedback, dialogs are more powerful and flexible. Dialogs should only be used if the additional flexibility is needed, since it implies more coding.

Showing a dialog needs a yaji.gui.View specification as above. At first, yaji.app.Application.create_dialog() creates a new yaji.gui.Dialog for a view specification, then yaji.Dialog.show() shows it to the user.

See the dialog.py sample application.

Internationalization

There are many aspects of internationalization, many of them handled automatically by the language infrastructures, if program code is straight-forward enough.

Translations

One of the other aspects is translating user interface texts, so the user interface can be shown in the native language of the user.

Application code can register text translations during initialization by an internal key and a translation in some languages for each text, like in the following:

class MyApplication(Application):

    def __init__(self):
        super().__init__()
        self.add_translations('Welcome', en="Welcome!", it="Benvenuto!", nl="Welkom!")

Other application code can refer to those texts like in the following example:

self.setbodyleft(View(label=TrStr('Welcome')))

If the user language is not available, the English text is shown.

Application stopping

The application shutdown procedure is a bit complicated internally, since there must be a stable and defined interaction between application code side and browser side in different situations. The application code can stop the application, the browser side can trigger it, and the user sometimes can just close all browser windows.

See yaji.app.Application.stop() for details.

Forcefully keeping a browser view open

The default behavior of Yaji is to try keeping one view open. When the user closes all browser windows, it will open a new one, so the user is forced to correctly stop the application. The stop_implicitly_when_browser_closed parameter of yaji.app.Application allows to control this behavior.

It is possible to temporarily override that choice for the execution of a code block with the yaji.app.Application.do_stop_implicitly_when_browser_closed() and yaji.app.Application.dont_stop_implicitly_when_browser_closed() context managers.

Closed notification

If the default behavior of forcefully keeping a browser view open is enabled, a re-opened view will show a notification. This notification explains why the browser opened again (and allows to close the application in some situations). The show_browser_closed_notification parameter of yaji.app.Application allows to control this behavior.

Shutdown dialog

In some situations, the application stops without closing all opened views. This usually happens whenever a view is opened in a usual web browser, since application shutdown will not close those browsers or tabs inside them. The default behavior is to show a shutdown dialog in those views, which disables the complete user interface and shows an generic information text. The skip_shutdown_dialog parameter of yaji.Application allows to control this behavior.

Browser side

A Yaji application is allowed to include browser side resources like JavaScripts or style sheets for more flexibility in user interface implementations and for more direct interactions with the user, avoiding expensive data transfer with the application code side.

This leaves the world of Python and the application code context. It is an advanced feature, which should be avoided if a similar implementation without browser side code is possible.

Adding browser side resources

The simplest way to include browser side resources is to locate a JavaScript in __foo.js and/or a style sheet in __foo.css in the same directory as the main application foo.py.

More resources can be add with yaji.app.Application.add_clientscript() and yaji.app.Application.add_clientstyle().

Directories for additional static second-level resources (e.g. images used in style sheets) can be added with yaji.app.Application.add_staticfile_location().

Custom widgets

One good reason for including a JavaScript is to implement a custom widget class, i.e. a class that directly or indirectly extends clove.Widget. Details about this are beyond the scope of this text. See the customwidget_simple.py sample application and the Clove API reference.

Yaji browser side API

There is a rich Yaji API on browser side, which provides mechanisms for data exchange with the application code side, user interface features, and more. Some of its basic features are introduced in the next chapters. There is a dedicated API reference for it below, where you can look up details about the functions mentioned in the following.

yaji.ready

An included JavaScript gets executed at an early step of initialization. The infrastructure is not ready for many things at this moment. yaji.ready calls a function once the initialization is finished. See the yaji_ready.py sample application.

Ajax

Ajax requests are a low-level way to communicate with the application code side. They can be done by the XMLHttpRequest web API or any other ones. There is also yaji.ajax(), which can be used for that.

On application code side, request handlers will answer Ajax requests. Non downstream request handlers can return an arbitrary serializable data structure that is passed back as response to browser side. Any Ajax API allows to retrieve and process those responses.

Request parameters

Ajax requests can carry parameters in the usual ways, either encoded in the url or in a POST body, which is usually abstracted by the Ajax API. On application code side, a request handler will receive those parameters by the arguments of the method call. The following example makes an Ajax request in browser code:

yaji.ajax({url: 'something', data: {x: 13, y: 37}});

The request handler can be defined like in the following example for receiving the parameters:

@RequestHandler.for_urls(auto=True)
def _do_something(self, x, y):
    do_something_spectacular_in(x, y)

Note

The request handler is required to take exactly the arguments it gets. Violating that is potentially a critical error. Since request handlers are just Python functions, their signatures can specify default values for arguments and variable arguments like **kwargs in order to solve this problem.

Request parameter types

By default, request arguments are passed to the request handlers as strings. Type annotations lead to automatic conversions, so the following example is working code:

@RequestHandler.for_urls(auto=True)
def _do_something(self, x: int, y: int):
    return 100 * x + y

See the requesthandler.py sample application. See yaji.app.Application.add_requestparam_type_converter() for custom types.

Request objects

Inside a request handler it is possible to access some extended aspects of the request, like the request url, and to participate on building the response in low-level ways, by a request object. See yaji.request.Request, yaji.app.Application.current_request() and the request.py sample application.

Client events

Client events are a low-level mechanism that allows the application code side to broadcast events to the open view(s). They should not to be confused with event bindings, which are a high-level mechanism working in the other direction.

A client event has a name that specifies the kind of event, and some arbitrary event data. Application code can trigger a client event like in the following example:

self.triggerevent('somethinghappened', id=1337)

See also yaji.app.Application.triggerevent().

Browser side code can register a handler during initialization like in the following example:

yaji.addEventHandler('somethinghappened', (data) => {
    doSomethingWith(data.id);
});

Appconfig

The appconfig mechanism has some similarities to data sources but is specialized for global values. It stores some arbitrary global application state data by a key name, backed on application code side, and provides an easy browser side API for consuming them. Application code can set a value like in the following example:

self.appconfig.setconfig('headertext', "Schildpad")

See also yaji.appconfig.AppConfig and yaji.app.Application.appconfig.

Browser side code can consume those values by registering a handler during initialization like in the following example:

yaji.appconfig.addHandler('headertext', (value) => {
    setMyFunnyHeader("Een " + value + "je");
});

Note

There is no risk in registering an appconfig handler after it was set by application code. When a handler is added, if there already is a value, the handler will be executed once with that value.

Curtains

Curtains are a way to work around the problems of the decoupling between application code side and browser side. In some defined situation, curtains allow to disable the user interface or to shield it in some other ways, until all transitions between application code side and browser sides are finished. This stops the user from interventing with the user interface while it is not guaranteed to be in a consistent state.

Curtains are implemented on browser side. There is a default yaji.Curtain implementation, which can also be subclassed for blocking the user interface in different ways (e.g disabling parts of it).

See the curtain.py sample application.

JSON serialization

JSON serialization is used heavily for data transfer from application code side to browser side (it is also used for some transfers from browser side) and in this direction can also be extended by serialization routines for custom types. Those extensions have an application side code (serialization) part and a browser side (deserialization) code part.

The former is realized by adding a _to_simple_repr_() method to the custom type, like in the following example:

class MyObject:
    ...

    def _to_simple_repr_(self):
        return 'MyObject', {'foo': self.__foo}

This method returns a deserializer name and a dictionary of arbitrary data that are serializable. The deserializer name can be equal to the class name on application code side, but it actually specifies the class on browser code side that handles the deserialization. Such a class can be like in the following example:

class MyObject {
    ...

    static _from_simple_repr_(data) {
        return new MyObject(data.foo, 42);
    }

}

Middleware

Middleware are a plugin mechanism for affecting internal request handling in a pluggable and potentially custom way. See yaji.core.Middleware, yaji.app.Application.add_middleware() and the middleware.py sample application.

There are also some methods of yaji.app.Application, often beginning with on, that can be overridden for controlling other aspects of low-level behavior.

Application takeover

Application takeover allows multiple Yaji applications to share a browser view by taking it over from a parent application on startup and releasing it when stopping.

This is a feature for exotic scenarios. It can avoid new windows or browser tabs for each new application instance when many applications run in an interleaved way.

See the takeover.py sample application.

Storing applications statically

For exotic scenarios, and only if the application implementation is compatible to it, yaji.app.Application.storeasstaticapplication() can be used for storing the entire application to a directory of static files. This can again be opened with a usual web browser, but also without any application backend running.

Logging

There is a Python logging logger used by Yaji for writing messages for diagnostics. It can also be used by application code. See yaji.core.Log and yaji.core.log.

Note

Setting the environment variable YAJI_LOGDEBUG to 1 causes a Yaji application to write verbose debug log messages to the terminal.

TODO colon js colon func refs