About
In the following we will briefly describe how the components in app
component system work.
Jinja Strings
The component system of app
is based in jinja2. This means that the components constructs strings following jinja2 syntax. We have a specific type Jinja
(which is a subtype of Str
) whose instances are the so-called jinja strings: Python strings preppended with the “jinja
” keyword.
So, more precisely, an instance of Jinja
is a string as follows (see jinja2 to discover the full valid syntax):
1my_jinja_string = """jinja
2{% for i in x %}
3<some html>
4 {% if y is True %}
5 <more html>
6 ...
7 </more html>
8 {% endif %}
9</some html>
10{% endfor %}
11"""
Above, x
and y
are jinja variables, which must be defined inside some context, for example as part of a component, before converting the jinja string to HTML (see the rendering documentation).
Components
The basic unities of app
component systems are, of course, the components. A component is a typed function (in the sense of typed) such that:
it returns a jinja string, so that its returning type is the
Jinja
type;it is decorated with the
@component
decorator.
Thus, a typical component is defined as follows:
1from typed import SomeType, OtherType
2from app import component, Jinja
3
4@component
5def my_comp(x: SomeType, y: OtherType, ...) -> Jinja:
6 ...
7 return """jinja
8{% for i in x %}
9<some html>
10 {% if y is True %}
11 <more html>
12 ...
13 </more html>
14 {% endif %}
15</some html>
16{% endfor %}
17"""
One should note that local variables of a component are automatically included in the context of the returning jinja string. This means that local variables defined in the body of a component can be directly used as jinja vars:
1from typed import SomeType, OtherType
2from app import component, Jinja
3
4@component
5def my_comp(x: SomeType, y: OtherType, ...) -> Jinja:
6 ...
7 some_var = some_value
8 ...
9 return """jinja
10{% for i in x %}
11<some html>
12 {% if y is True %}
13 <more html>
14 {{ some_var }}
15 </more html>
16 {% endif %}
17</some html>
18{% endfor %}
19"""
Remark 1. There is the type
COMPONENT
whose instances are precisely theapp
components. It is constructed as a subtype ofTypedFunc(Any, cod=Jinja)
, i.e., of all typed functions whose codomain isJinja
.
Dependences
Components can depend one each others. More precisely, a component has a special optional variable __depends_on__
, which lists other already defined components of which the defining component depends on.
One time listed in the __depends_on__
variable, the dependent components are included into the context, so that they are turned into jinja vars and can be called inside the jinja string of the defining component, as follows:
1from app import Jinja, Tag, component
2
3@component
4def comp_1(...) -> Jinja:
5 ...
6 return """jinja
7 ...
8"""
9
10@component
11def comp_2(...) -> Tag('some-tag'):
12 ...
13 return """jinja
14<some-tag>
15 ...
16</some-tag>
17"""
18
19@component
20def main_comp(..., __depends_on__=[comp_1, comp_2]) -> Jinja:
21 ...
22 return """jinja
23 ...
24{{ comp_1(...) }}
25 ...
26{{ comp_2(...) }}
27"""
Recall that components are typed functions, which means that all their arguments must have type annotations, which are checked at runtime. The
__depends_on__
, however, is an exception: if a type annotation is not provided, thenList(COMPONENT)
is automatically attached to it. On the other hand, if a type annotation is provided, it must be a subtype ofList(COMPONENT)
.
Free Variables
When a jinja var is in the jinja string returned by a component, it typically corresponds to one of the following cases:
it is component argument;
it is a local variable defined in the body of the component;
it is another component listed in the
__depends_on__
argument.
A jinja var which does not satisfies some of the conditions above is called a free jinja var.
Remark 2. It is not a best practice to have free jinja vars in a component. Indeed, as will de discussed in Rendering, a free jinja var need to be explicitly included in the context during the rendering of a context. The problem is that, unlike component arguments, they are not true Python components, which makes difficult to remember their existence.
Inners
In app
, the components may have another special kind of variables: the inner ones. They are necessarily of type Inner
, and work as placeholders for future inserts inside the component.
1from app import component, Tag, Inner
2
3@component
4def my_inner_comp(..., inner: Inner, ...) -> Tag('some-tag'):
5 ...
6 return """jinja
7<some-tag>
8 {{ inner }}
9</some-tag>
10"""
The type Inner
is a presentation of the Jinja
type.
The components may also admit content vars (of type
Content
), characterizing them as static components, which can be used to introducedMarkdown
andRST
content inside the component. This will be discussed in Statics.
Component Factory
There is a type factory Component: Int -> SUB(COMPONENT)
that to each integer n>=0
returns the subtype Component(n)
of COMPONENT
of all components that have exactly n
inner vars.
This decomposes the type COMPONENT
into distinct subtypes of components with a fixed amount of inner
variables.
So, for example, for the my_inner_comp
defined above one would have:
1from app import Component
2
3print(isinstance(my_inner_comp, Component(1)) # will return 'True'
4print(isinstance(my_inner_comp, Component(0)) # will return 'False'
By definition, if
n<0
, thenComponent(n)
is set precisely toCOMPONENT
, meaning that the component may have any number ofinner
variables, including zero.
Attributes
The type COMPONENT
of all components has some properties, which define some attributes to the components:
attribute |
description |
---|---|
.jinja |
code of the component _jinja string_ |
.jinja_vars |
tuple of _jinja vars_ |
.jinja_free_vars |
tuple of _jinja free vars_ |
.inner_args |
tuple of _inner args_ |
.content_args |
tuple of _content_args_ |
.tags |
the tuple of tags that defines the component |
Operations
There are three main operations involving components:
join: COMPONENT x COMPONENT -> COMPONENT
:receive a tuple of components and creates a new component whose jinja string is the join of the jinja strings of the provided components;
concat: Component(1) x COMPONENT -> COMPONENT
:receive a component with a single inner var and another arbitrary component, producing a new component whose jinja str is obtained by replacing the placeholder given by inner var in the first component with the jinja string of the second component.
eval: COMPONENT x Dict(Any) -> COMPONENT
:receive a component and a list of key-values and returns the component obtained by fixing each variable associated to a key with the corresponding value, leaving the other variables unchanged.
The intuition for each of such operations is as follows:
join
: put a component after other componentconcat
: put a component inside other componenteval
: from a component, fixes some part
So, for example, consider the following generic components:
1from typed import SomeType, OtherType
2from app import Jinja, component, Tag, Inner, join, concat, eval
3
4@component
5def some_comp(x: SomeType, ...) -> Jinja
6 ...
7 return """jinja
8{{ contents of 'some_comp' jinja string }}
9"""
10
11@component
12def inner_comp(a: OtherType, inner: Inner, ...) -> Tag('some-tag')
13 ...
14 return """jinja
15<some-tag>
16 {{ inner }}
17</some-tag>
18"""
The resulting joined component join(some_comp, inner_comp)
is equivalent to the following component:
1@component
2def joined_comp(x: SomeType, ..., a: OtherType, inner: Inner, ...) -> Jinja:
3 return """jinja
4{{ contents of 'some_comp' jinja string }}
5<some-tag>
6 {{ inner }}
7</some-tag>
8"""
On the other hand, concat(inner_comp, some_comp)
is equivalent to:
1@component
2def concat_comp(a: OtherType, x: SomeType, ...) -> Tag('some-tag'):
3 return """jinja
4<some-tag>
5 {{ contents of 'some_comp' jinja string }}
6</some-tag>
7"""
Finally, eval(inner_comp, inner="blablabla")
is the same as defining the component below.
1@component
2def eval_comp(a: OtherType, ...) -> Tag('some-tag'):
3 return """jinja
4<some-tag>
5 blablabla
6</some-tag>
7"""
In both
join
andconcat
operations, the__depends_on__
variable is the obtained as the concatenation of the__depends_on__
of the underlying components. In theeval
operation, on the other hand, the__depends_on__
is maintained the same.
Arithmetic
In the type COMPONENT
, the operations join
and concat
corresponds, respectively to implementations of the class functions __sum__
and __mul__
. This means that instead of writing join(comp_1, comp_2)
you can just write comp_1 + comp_2
. Similarly, instead of concat(comp_1, comp_2)
, you can write comp_1 * comp_2
.
Therefore, the COMPONENT
type has an internal “component arithmetic” that can be used to build complex components from smaller ones.