wiki:ComponentModel

Version 5 (modified by apoirier, 7 years ago) (diff)

--

The Nagare component model

What is a component ?

A component is an object, instance of the Component class in component.py#component.Component.

A component has one or multiple rendering methods. They are used to generate a HTML, XHTML, XML or any other views on it.

A component knows how to replace itself by an other component, either permanently or temporary.

How to create a component ?

With a lots of components based frameworks, you need to make your classes inherit from a Component class. Nagare is using composition instead of inheritance so you only need to wrap your python object into a Component instance to obtain a component. That's mean you can make a component from a "Plain Old Python Object" without needed to change its code.

From example, starting with a pure Python Counter class :

class Counter:
    def __init__(self):
        self.value = 0

    def increase(self):
        self.value += 1

    def decrease(self):
        self.value -= 1

to create a Counter component, you only need to do:

from nagare import component
counter1 = component.Component(Counter())

How to associate views to a component ?

The nagare.presentation presentation service of the framework keeps track of the views associated to a class.

The views are plain Python methods and multiple views can be registered on a class, each with a different model name.

The views are associated to a class using the presentation.render_for decorator. Then when the presentation service is asked to render a component, with a given model, the view for this model is called with :

  1. the object (the inner object of the component)
  2. a renderer
  3. the component
  4. the model asked

and the method will generally returns a tree of DOM objects, build by using the RendererObjects API.

The following code associates a default view (the view_by_default method) and a view for the xml model (the xml_view method) to the class AClass:

from nagare import presentation

@presentation.render_for(AClass)
def view_by_default(self, h, component, model, *args):
  # Here, generate and return a view, typically a DOM tree

@presentation.render_for(AClass, model='xml')
def xml_view(self, h, component, model, *args):
  # Here, generate and return a view, typically a DOM tree

Note

In general, we append *args to the signature of the view methods for compatibility with some future parameters addition. Furthermore if you're not using, for example, the component and the model parameters into your rendering process but only the self and h parameters, then you could defined your view method as a simpler def render(self, h, *args)

The views for a class are defined and added externally to the class itself, even from an other file if you want. So again, views can be added without any modifications of the class.

For example, a default HTML view on our Counter objects could be:

from nagare import presentation

@presentation.render_for(Counter)
def render(self, h, *args):
    with h.div:
        h << 'Value: ' << self.value << h.br
        h << h.a('--')
        h << ' | '
        h << h.a('++')

    return h.root

and an other simpler HTML view, called static, could be:

@presentation.render_for(Counter, model='static')
def render(self, h, *args):
    return h.b(self.value)

How to render a component ?

To render a component, calls its render() method, with a renderer and an optional model parameter:

>>> from nagare.namespaces import xhtml

>>> counter1 = Component(Counter())

>>> h = xhtml.Renderer()
>>> tree = counter1.render(h)
>>> tree.write_htmlstring()
'<div>Value: 0<br><a>--</a> | <a>++</a></div>'

>>> h = xhtml.Renderer()
>>> tree = counter1.render(h, model='static')
>>> tree.write_htmlstring()
'<b>0</b>'

But it's simpler to add the component as the child of a DOM object and let Nagare automatically creates a new renderer and call the view of the component. So:

>>> tree = h.div(counter1)
>>> tree.write_htmlstring()
'<div><div>Value: 0<br><a>--</a> | <a>++</a></div></div>'

is a shortcut for explicitly calling the default view:

>>> tree = h.div(counter1.render(h))
>>> tree.write_htmlstring()
'<div><div>Value: 0<br><a>--</a> | <a>++</a></div></div>'

How to build a compound component ?

A component can be build from other existing components. The inner components simply are normal Python attributes of the compound component.

First we create a ColorChooser component, a component displaying four clickable cells with differents colors:

class ColorChooser:
    pass

@presentation.render_for(ColorChooser)
def render(self, h, binding, *args):
    return (
            h.a('X', style='background-color: #4B96FF'),
            h.a('X', style='background-color: #FF8FDF'),
            h.a('X', style='background-color: #9EFF5D'),
            h.a('X', style='background-color: #ffffff')
           )

then a ColorText component, a text displayed with a given background color:

class ColorText:
    def __init__(self, text, color='#ffffff'):
        self.color = color
        self.text = text

@presentation.render_for(ColorText)
def render(self, h, *args):
    return h.span(self.text, style='background-color:' + self.color)

Composition of embedded components

Views composition

The simplest method to build a compound component is to add a view that embeds the views of the inner components:

from nagare.component import Component

# ``App`` is the compound component
class App:
    def __init__(self):
        # ``App`` embeds two inner components
        self.color_chooser_comp = Component(ColorChooser())
        self.text = ColorText('Hello world !')
        self.text_comp = Component(self.text)

@presentation.render_for(App)
def render(self, h, *args):
    with h.div:
        # The default view of ``App`` renders the default views of its
        # inner components
        h << self.color_chooser_comp
        h << h.hr
        h << self.text_comp

    return h.root

Launching the application with:

<NAGARE_HOME>/bin/nagare-admin serve-module color.py:App color

and browsing to http://localhost:8080/color, gives you:

X X X X
Hello world !

Logic composition

After creating the view of the application from views of its inner components, we will now create the logic of the application by combinating the logic of the inner components.

First, the ColorChooser, on a click on a color, now returns the selected color. For this purpose, we register a callback (see CallbacksAndForms) on the link items, that calls the answer() method of the ColorChooser component, with the value to return:

@presentation.render_for(ColorChooser)
def render(self, h, binding, *args):
    return (
            h.a('X', style='background-color: #4B96FF').action(lambda: binding.answer('#4B96FF')),
            h.a('X', style='background-color: #FF8FDF').action(lambda: binding.answer('#FF8FDF')),
            h.a('X', style='background-color: #9EFF5D').action(lambda: binding.answer('#9EFF5D')),
            h.a('X', style='background-color: #ffffff').action(lambda: binding.answer('#ffffff'))
           )

Note

Of course, this view could be written in a more compact Python code:

for color in ('#4B96FF', '#FF8FDF', '#9EFF5D', '#ffffff'):
    h << h.a('X', 'style' : 'background-color: ' + color).action(lambda color=color: binding.answer(color))

return h.root

second, we add a set_color() method to our ColorText object:

class ColorText:
    def __init__(self, text, color='#ffffff'):
        self.color = color
        self.text = text

    def set_color(self, color):
        self.color = color

and, finally, the App object binds the answer() method of the ColorChooser component to the set_color() method of the ColorText object, using the on_answer() method of the ColorChooser component:

class App:
    def __init__(self):
        self.color_chooser_comp = Component(ColorChooser())
        self.text = ColorText('Hello world !')
        self.text_comp = Component(self.text)

        self.color_chooser_comp.on_answer(self.text.set_color)

Now, clicking on a color immediatly change the background color of the text.

Permanently replacing a component

A component knows how to replace itself into the components graph with its becomes() method, which is called with the object or the component to be replaced by.

For example, our App component is now changed to only have one inner component, the content component:

class App:
    def __init__(self):
        self.color_chooser = ColorChooser()
        self.text = ColorText('Hello world !')

        self.content = Component(self.color_chooser)
        self.content.on_answer(self.text.set_color)

and its default view displays a little menu to choose between the ColorChooser object or the ColorText to display. The actions on the link of this menu simply replace the content component:

@presentation.render_for(App)
def render(self, h, *args):
    with h.div:
        # On a click on this link, replace the ``content`` inner object
        # by the ``color_chooser`` object
        h << h.a('The Color chooser').action(lambda: self.content.becomes(self.color_chooser))

        h << ' | '

        # On a click on this link, replace the ``content`` inner object
        # by the ``text`` object
        h << h.a('The text').action(lambda: self.content.becomes(self.text))

        h << h.hr
        # Render the ``content`` component which can have ``color_chooser``
        # or ``text`` as inner object
        h << self.content

    return h.root

So now a little menu of two items is displayed. Clicking on the items display the color_chooser or the text object. And, clicking on a color in the color_chooser change again the color of the text object:

Temporary replacing a component

Permanently replacing a component is like a "goto" in some programming languages. Temporary replacing a component is like a "call": the component is replaced, but as soon as the replacing component calls its answer(return_value) method, the previous component comes back in the component graph and receives the returned value.

For example, our App component can be changed to first displays the text_comp component and a link to choose its color. Once the link is clicked, the App component temporary replaces itself by the color_chooser object. Remenber that, once a color is selected, the color_chooser component do an answer(color). So the App component is restored, receives the selected color and change the color of the text:

class App:
    def __init__(self):
        self.color_chooser = ColorChooser()
        self.text = ColorText('Hello world !')

        self.text_comp = Component(self.text)

    def change_color(self, component):
        # When the 'Choose a color' link is clicked, temporary replace
        # the ``App`` component (here the ``component`` parameter) by the
        # ``color_chooser``
        #
        # When the ``color_chooser`` answers, the ``App`` component is
        # restored and the control flow continues: the color answered is
        # set to the text
        new_color = component.call(self.color_chooser)
        self.text.set_color(new_color)

 @presentation.render_for(App)
 def render(self, h, component, *args):
     with h.div:
         h << h.a('Choose a color').action(lambda: self.change_color(component))

         h << h.hr
         h << self.text_comp

     return h.root

How to build an application ?

You already did it !

With Nagare, an application is only a compound component, a graph of components.

And navigating into the application, by clicking on links or by submitting forms, is activating callbacks that permanently or temporary modify this graph.