Coming soon Rapidly build modern web apps all by yourself

using reusable UI components written in Python and HTML

What is Jembe?

Python web framework build on top of Flask, designed for:

  • Rapid development using components, actions and events;
  • Easy creation of reusable components;
  • No need to write too much JavaScript code;
  • Renders HTML on server and partially updates clients page.
Use Jembe, if You're:
  • Developing apps all by yourself or in very small teams;
  • Tired of doing “same thing” in backend and in frontend;
  • Already coding or don't mind coding in Python?

Code Examples

List of projects

Filter list by typing project name in search box.

from jembe import Component
from ..jmb import jmb
from ..db import db
from ..models import Project

@jmb.page("projects")
class Projects(Component):
    """Displays searchable list of projects"""

    def __init__(self, search: str = ""):
        """
        Defines "search" to be a component state variable.

        "Search" state variables is accessible and editable using self.state.search
        """
        super().__init__()

    def display(self):
        self.projects = db.session.query(Project).order_by(Project.id.desc())
        if self.state.search != "":
            self.projects = self.projects.filter(
                Project.name.ilike("%{}%".format(self.state.search))
            )
        return super().display()
<html>
<head>
    <title>Projects</title>
</head>
<body>
    <h1>Projects</h1>
    <!-- Associates "search" state variable with value of input field,  
         forcing a component to redisplay when value changes. -->
    <input jmb-model=”search” type=”text” placeholder=”Search...”/>
    <ul>
    {% for project in projects %}
        <li>{{project.name}}</li>
    {% endfor %}
    </ul>

    <script src="{{ url_for('jembe.static', filename='js/jembe.js') }}" defer></script> 
</body>
</html>
from ..db import db
import sqlalchemy as sa

class Project(db.Model):
    __tablename__ = "projects"

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String(150), nullable=False, unique=True)
    description = sa.Column(sa.Text)

    def __str__(self) -> str:
        return self.name

Paginated list of projects

Adding pagination to project list.

from math import ceil
from jembe import Component
from ..jmb import jmb
from ..db import db
from ..models import Project

@jmb.page(
    "projects_paginated", 
    Component.Config(url_query_params={"p": "page", "s": "search"})
)
class ProjectsPaginated(Component):
    """
    Displays searchable and paginated list of projects.
    
    @jmb.page decorator, configures "page" and "search" state variables 
    to be used as query parameters "p" and "s" when building url.
    """

    page_size = 10

    def __init__(self, search: str = "", page: int = 0):
        """Defines "search" and "page" state variables"""
        super().__init__()

    def display(self):
        self.projects = db.session.query(Project).order_by(Project.id.desc())

        # apply search filter
        if self.state.search != "":
            self.projects = self.projects.filter(
                Project.name.ilike("%{}%".format(self.state.search))
            )

        # apply pagination manually
        self.total_records = self.projects.count()
        self.total_pages = ceil(self.total_records / self.page_size)
        if self.state.page > self.total_pages - 1:
            self.state.page = self.total_pages - 1
        if self.state.page < 0:
            self.state.page = 0
        self.start_record_index = self.state.page * self.page_size
        self.end_record_index = self.start_record_index + self.page_size
        if self.end_record_index > self.total_records:
            self.end_record_index = self.total_records
        self.projects = self.projects[self.start_record_index : self.end_record_index]

        return super().display()
<html>
<head>
    <title>Projects Paginated</title>
</head>
<body>
    <h1>Projects</h1>
    <!-- Explicitly setting "search" state variable to input value and "page" to zero
         when input event is fired (when value of input has been changed) -->
    <input jmb-on:input="page=0;search=$self.value;" value="{{search}}" type="text" placeholder="Search...">
    <ul>
    {% for project in projects  %}
        <li>{{project.name}}</li>
    {% endfor %}
    </ul>
    <nav>
        <button jmb-on:click="page = page - 1" {% if page == 0 %} disabled {% endif %}>
            Prev
        </button>
        Page {{page + 1 if total_pages > 0 else 0}} of {{total_pages}} ({{total_records}} projects)
        <button jmb-on:click="page = page + 1" {% if page == total_pages - 1 or total_pages == 0 %}disabled{% endif %}>
            Next
        </button>
    </nav>

    <script src="{{ url_for('jembe.static', filename='js/jembe.js') }}" defer></script> 
</body>
</html>
from ..db import db
import sqlalchemy as sa

class Project(db.Model):
    __tablename__ = "projects"

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String(150), nullable=False, unique=True)
    description = sa.Column(sa.Text)

    def __str__(self) -> str:
        return self.name

Editable list of projects

Add edit component to list of projects.

from typing import Optional

from functools import cached_property
from jembe import Component, listener, action
from ..jmb import jmb
from ..db import db
from ..models import Project
from .projects_paginated import ProjectsPaginated


class ProjectEdit(Component):
    """Displays edit form and save changes to database"""

    def __init__(
        self, id: int, name: Optional[str] = None, description: Optional[str] = None
    ):
        self.error = None
        super().__init__()

    @cached_property
    def project(self):
        return db.session.query(Project).get(self.state.id)

    @action
    def save(self):
        """
            Saves changes to database.

            Actions can be called directly from html,
            similary to changing state variables
        """
        if self.state.name != "":
            self.project.name = self.state.name
            self.project.description = self.state.description
            db.session.commit()
            self.emit("save", project=self.project)
            # when an action returns False 
            # component will not be redisplayed even if 
            # state variables are changed
            return False
        self.error = "Name is required"

    @action
    def cancel(self):
        self.emit("cancel")
        return False

    def display(self):
        if self.state.name is None:
            # when displaed for the first time initialise name and description
            self.state.name = self.project.name
            self.state.description = self.project.description
        return super().display()


@jmb.page(
    "projects_editable", Component.Config(components={"edit": ProjectEdit}),
)
class ProjectsEditable(ProjectsPaginated):
    """
    Extends existing ProjectsPaginated component
    to add support for displaying "edit" subcomponent.
    
    @jmb.page decorators add "ProjectEdit"  as "edit" subcomponent.
    """

    def __init__(
        self, search: str = "", page: int = 0, display_mode: Optional[str] = None
    ):
        """display_mode: defines if project list or edit component are displayed"""
        super().__init__(search=search, page=page)
        if (
            display_mode is not None
            and display_mode not in self._config.components.keys()
        ):
            display_mode = None

    @listener(event="_display", source="./edit")
    def on_display_edit(self, event):
        """When "edit" subcomponent is displayed update display_mode"""
        self.state.display_mode = "edit"

    @listener(event=["cancel", "save"], source="./edit")
    def on_child_save_or_cancel(self, event):
        """When "edit" subcomponent emits "save" or "cancel" update display_mode"""
        self.state.display_mode = None

    def display(self):
        if self.state.display_mode is not None:
            # no need to query database and recalculate pagination
            # if only edit component is displayed
            return self.render_template()
        else:
            return super().display()
<html>
<head>
    <title>Projects Editable</title>
</head>
<body>
    {% if display_mode is none %}
        {% include "projects_editable/_list.html" %} 
    {% else %}
        {{component(display_mode)}}
    {% endif %}

    <script src="{{ url_for('jembe.static', filename='js/jembe.js') }}" defer></script> 
</body>
</html>
<h1>Projects</h1>
<input jmb-on:input="page=0;search=$self.value;" value="{{search}}" type="text" placeholder="Search...">
<ul>
{% for project in projects  %}
    <li>{{project.name}}</li>
{% endfor %}
</ul>
<nav>
    <button jmb-on:click="page = page - 1" {% if page == 0 %} disabled {% endif %}>
        Prev
    </button>
    Page {{page + 1 if total_pages > 0 else 0}} of {{total_pages}} ({{total_records}} projects)
    <button jmb-on:click="page = page + 1" {% if page == total_pages - 1 or total_pages == 0 %}disabled{% endif %}>
        Next
    </button>
</nav>
<h1>Edit: {{project.name}}</h1>
<label>
    Name
    <!-- defer modifier dellays sending request to server and redisplaying component -->
    <input type="text" value="{{name}}" jmb-on:change.defer="name=$self.value">
    {% if error %}<div style="color:red;">{{error}}</div>{% endif %}
</label>
<label>
    Description
    <textarea jmb-model.defer="description"></textarea>
</label>
<div>
    <button jmb-on:click="save()">Save</button>
    <button jmb-on:click="cancel()">Cancel</button>
</div>        

Use Reusable Components

Build complex apps using reusable components.

Reusable components add extra configuration params.

from jembe import config, Component
from ..jmb import jmb
from ..components import CPage, CCrudListRecords, CEdit, FormBase, CView, CCreate, CDelete,
from ..db import db
from ..models import Project, Note
from wtforms import validators as v, StringField, TextAreaField, SelectField
import sqlalchemy as sa


class ProjectForm(FormBase):
    """Defines project form using wtforms library"""
    name = StringField(
        validators=[v.DataRequired(), v.Length(max=Project.name.type.length)]
    )
    description = TextAreaField()

    def mount(self, component: "Component") -> "FormBase":
        if isinstance(component, CView):
            # disable all fields when form is used in view component
            self.disable_field(self.name)
            self.disable_field(self.description)
        return super().mount(component)


@config(
    CCrudListRecords.Config(
        # database engine
        db=db,
        # db query to list records
        query=sa.orm.Query([Project.id, Project.name]).order_by(Project.id.desc()),
        # search condition to be add to query
        search_filter=lambda search: Project.name.ilike("%{}%".format(search)),
        # links in component header
        actions=[lambda self: ("Create", self.component("create"))],
        # links for every record in list
        record_actions=[
            lambda self, record: ("View", self.component("view", id=record.id)),
            lambda self, record: ("Edit", self.component("edit", id=record.id)),
            lambda self, record: ("Delete", self.component("delete", id=record.id)),
        ],
        # subcomonents and its configuration
        components={
            "edit": (CEdit, CEdit.Config(db=db, model=Project, form=ProjectForm)),
            "create": (CCreate, CCreate.Config(db=db, model=Project, form=ProjectForm)),
            "view": (CView, CView.Config(db=db, model=Project, form=ProjectForm)),
            "delete": (CDelete, CDelete.Config(db=db, model=Project)),
        },
    )
)
class CListProjects(CCrudListRecords):
    pass


class NoteForm(FormBase):
    name = StringField(
        validators=[v.DataRequired(), v.Length(max=Project.name.type.length)]
    )
    description = TextAreaField()
    project_id = SelectField("Project", coerce=int)

    def mount(self, component: "Component") -> "FormBase":
        if isinstance(component, CView):
            self.disable_field(self.name)
            self.disable_field(self.description)
            self.disable_field(self.project_id)
            # displays only selected project inside view component
            self.project_id.choices = [
                (p.id, p.name)
                for p in db.session.query(Project).filter(
                    Project.id == self.project_id.data
                )
            ]
        else:
            # add projects in select project field
            self.project_id.choices = [
                (p.id, p.name) for p in db.session.query(Project)
            ]
        return super().mount(component)


@config(
    CCrudListRecords.Config(
        db=db,
        query=(
            sa.orm.Query(
                [
                    Note.id,
                    Note.name,
                    sa.sql.func.substr(Project.name, 1, 25).label("project"),
                ]
            )
            .join(Note.project)
            .order_by(Note.id.desc())
        ),
        search_filter=lambda search: Note.name.ilike("%{}%".format(search)),
        actions=[lambda self: ("Create", self.component("create"))],
        record_actions=[
            lambda self, record: ("View", self.component("view", id=record.id),),
            lambda self, record: ("Edit", self.component("edit", id=record.id),),
            lambda self, record: ("Delete", self.component("delete", id=record.id),),
        ],
        components={
            "edit": (CEdit, CEdit.Config(db=db, model=Note, form=NoteForm)),
            "create": (CCreate, CCreate.Config(db=db, model=Note, form=NoteForm),),
            "view": (CView, CView.Config(db=db, model=Note, form=NoteForm)),
            "delete": (CDelete, CDelete.Config(db=db, model=Note)),
        },
    )
)
class CListNotes(CCrudListRecords):
    pass


@jmb.page(
    "demo_reusable",
    CPage.Config(components={"projects": CListProjects, "notes": CListNotes}),
)
class DemoReusableComponents(CPage):
    pass
"""
Example on how reusable component can be build.

Source code of all components are avaiable on: 
https://github.com/Jembe/jembe-io-examples
"""

from functools import cached_property
from typing import Any, TYPE_CHECKING, Optional, Type, Union, Iterable, Dict, Callable, Tuple
from jembe import Component, run_only_once, action
from .form import FormBase

if TYPE_CHECKING:
    from flask_sqlalchemy import SQLAlchemy, Model
    from jembe import RedisplayFlag, ComponentConfig, ComponentRef
    import sqlalchemy as sa


class CEdit(Component):
    class Config(Component.Config):
        """To add new configuration parameters we extends Component.Config"""
        default_template = "components/edit.html"

        def __init__(
            self,
            db: "SQLAlchemy",
            form: Type["FormBase"],
            model: "Model",
            template: Optional[Union[str, Iterable[str]]] = None,
            components: Optional[Dict[str, "ComponentRef"]] = None,
            inject_into_components: Optional[
                Callable[["Component", "ComponentConfig"], dict]
            ] = None,
            redisplay: Tuple["RedisplayFlag", ...] = (),
            changes_url: bool = True,
            url_query_params: Optional[Dict[str, str]] = None,
        ):
            """
                New configuration parameters are added as __init__ arguments.

                We are relaying on code editor to help us create this __init__ method.
            """
            self.db = db
            self.form = form
            self.model = model

            if template is None:
                # when template is list, 
                # first avaiable template is used
                template = ("", self.default_template)

            super().__init__(
                template=template,
                components=components,
                inject_into_components=inject_into_components,
                redisplay=redisplay,
                changes_url=changes_url,
                url_query_params=url_query_params,
            )

    # This is to help code editor autocomplete suggestions
    _config: Config

    def __init__(self, id: int, form: Optional[FormBase] = None):
        """Defines "id" and "form" state variables"""
        super().__init__()

    @classmethod
    def load_init_param(cls, config: "ComponentConfig", name: str, value: Any) -> Any:
        """ "Deserielise" form recived from client"""
        if name == "form":
            # otherwise it will use FormBase.load_init_param
            return config.form.load_init_param(value)
        return super().load_init_param(config, name, value)

    @cached_property
    def record(self) -> "Model":
        return self._config.db.session.query(self._config.model).get(self.state.id)

    @run_only_once
    def mount(self):
        if self.state.form is None:
            self.state.form = self._config.form(obj=self.record)
        self.state.form.mount(self)

    @action
    def save(self):
        self.mount()
        if self.state.form.validate():
            try:
                self.state.form.populate_obj(self.record)
                self._config.db.session.commit()
                self.emit("save", record=self.record, id=self.record.id)
                self.emit(
                    "pushNotification", message="{} saved".format(str(self.record))
                )
                # do not execute display after sucessfull save
                return False
            except sa.exc.SQLAlchemyError as error:
                self.emit(
                    "pushNotification",
                    message=str(getattr(error, "orig", error)),
                    level="error",
                )

        self._config.db.session.rollback()
        # force redisplay of this edit component
        return True

    @action
    def cancel(self):
        self.emit("cancel")
        # force not redisplaying this edit component
        return False

    def display(self):
        self.mount()
        return super().display()

    @property
    def title(self):
        return "Edit: {}".format(self.record)

How does Jembe works?

  • Initial http request gets the whole page, composed from one or multiple components;
  • On user action, current state of all components and user action are send back to server using AJAX;
  • Server executes action and re-renders components whose state is changed;
  • Browser mutates DOM according to changes in re-displayed components.

When will it be ready?

In the 2022, as soon as documentation is written.
Sign up to be notified when Jembe launches.

    I'll send only information about Jembe project, no spam. Unsubcribe at any time.