1
0
Fork 0

feat: simplify base/page/form template structure; add docs

i'm sure there is more to document yet but this gets us started
This commit is contained in:
Lance Edgar 2024-08-19 16:52:16 -05:00
parent 3579bebdeb
commit fce0de5d30
14 changed files with 445 additions and 153 deletions

View file

@ -2,15 +2,7 @@
Documentation Documentation
============= =============
TODO
..
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
install/index templates/index
config/index
cli/index
handlers/index
providers/index
db/index

View file

@ -0,0 +1,251 @@
Base Templates
==============
This describes the base templates. When creating a custom page
template, you most often need to inherit from one of these:
* :ref:`page_base_template`
* :ref:`form_base_template`
* :ref:`master_base_templates`
.. note::
Any of these templates may be overridden; see
:ref:`mako-template-override`.
Global Base
~~~~~~~~~~~
There is exactly one "true base template" for the web app, designated
as: ``/base.mako``
The default base template is ``wuttaweb:templates/base.mako`` and all
page templates inherit from it. However they inherit it by *name*
only (``/base.mako``) - therefore if you override this via custom
template search paths, effectively you have changed the **theme**.
In addition to general layout/structure, this template is reponsible
for creating the Vue app which encompasses the whole of every page.
It also establishes the ``WholePage`` component which is the Vue app's
one and only child component.
(``WholePage`` in turn will have other children, for page content.)
There is usually no need to define a template which inherits directly
from ``/base.mako``, rather you should inherit from ``/page.mako``
(see next section) or similar.
As pertains to Vue component logic, there are 3 blocks which you may
find a need to override. These are defined by ``/base.mako`` so will
apply to *all* templates:
* ``render_vue_templates()``
* ``modify_vue_vars()``
* ``make_vue_components()``
Most often it is necessary to customize ``modify_vue_vars()`` but keep
reading for an example.
.. _page_base_template:
Page Base
~~~~~~~~~
The common base template for pages, designated as: ``/page.mako``
This extends the Vue logic from ``/base.mako`` by establishing
``ThisPage`` component, which wraps all content within the current
page.
The final structure then is conceptually like:
.. code-block:: html
<div id="app">
<whole-page>
<!-- menu etc. -->
<this-page>
<!-- page contents -->
</this-page>
</whole-page>
</div>
Simple usage is to create a template which inherits from
``/page.mako`` and defines a ``page_content()`` block, e.g.:
.. code-block:: mako
<%inherit file="/page.mako" />
<%def name="page_content()">
<p>hello world!</p>
</%def>
The default ``/page.mako`` logic knows where to render the
``page_content()`` block so that it fits properly into the
component/layout structure.
Often you may need to customize Vue component logic for a page; this
is done by defining one of the blocks mentioned in previous section.
Here is a simple example which shows how this works:
.. code-block:: mako
<%inherit file="/page.mako" />
<%def name="page_content()">
<b-field label="Foo">
<b-input v-model="foo" />
</b-field>
<b-field>
<b-button @click="alertFoo()">
Alert
</b-button>
</b-field>
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
// nb. this becomes ThisPage.data.foo
ThisPageData.foo = 'bar'
ThisPage.methods.alertFoo = function() {
alert("value of foo is: " + this.foo)
}
</script>
</%def>
You can see that ``page_content()`` is able to reference things from
``ThisPage`` component, while the ``modify_vue_vars()`` block is used
to define those same things on the component.
.. _form_base_template:
Form Base
~~~~~~~~~
The common base template for pages with a form, designated as:
``/form.mako``
This expects the context dict to contain ``'form'`` which points to a
:class:`~wuttaweb.forms.base.Form` instance.
This template extends the Vue logic from ``/page.mako`` by
establishing a Vue component specific to the form object.
The final structure then is conceptually like:
.. code-block:: html
<div id="app">
<whole-page>
<!-- menu etc. -->
<this-page>
<wutta-form>
<!-- fields etc. -->
</wutta-form>
</this-page>
</whole-page>
</div>
A simple example which assumes one of the form fields exposes a button
with click event that triggers ``alertFoo()`` method on the form
component:
.. code-block:: mako
<%inherit file="/form.mako" />
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
// nb. this becomes e.g. WuttaForm.foo when component is created
${form.vue_component}Data.foo = 'bar'
${form.vue_component}.methods.alertFoo = function() {
alert("value of foo is: " + this.foo)
}
</script>
</%def>
.. note::
By default, ``${form.vue_compoment}`` is rendered as ``WuttaForm``
but that is not guaranteed. You should resist the temptation to
hard-code that; always use ``${form.vue_component}`` and (where
applicable) ``${form.vue_tagname}``.
The reason for this is to allow multiple forms to exist on a single
page, each with a separate Vue component. (Which is not shown in
the above example.)
See also :attr:`~wuttaweb.forms.base.Form.vue_component` and
:attr:`~wuttaweb.forms.base.Form.vue_tagname`.
.. _master_base_templates:
Master Base
~~~~~~~~~~~
These templates are for use with
:class:`~wuttaweb.views.master.MasterView`. Each is the default
template used for the corresponding route/view, unless a more specific
template is defined.
The "index" template is unique in that it is (usually) for listing the
model data:
* ``/master/index.mako``
The "form" template is just a base template, does not directly
correspond to a route/view. Other CRUD templates inherit from it.
This inherits from ``/form.mako`` (see previous section).
* ``/master/form.mako``
These CRUD templates inherit from ``/master/form.mako`` and so
require a ``'form'`` in the context dict.
* ``/master/create.mako``
* ``/master/view.mako``
* ``/master/edit.mako``
* ``/master/delete.mako``
The "configure" template is for master views which have a
configuration page.
* ``/master/configure.mako``
Usage for these is not significantly different from the ones shown
above, in cases where you actually need to override the template.
As an example let's say you have defined a ``WidgetMasterView`` class
and want to override its "view" template. You would then create a
file as ``/widgets/view.mako`` (within your templates folder) and
be sure to inherit from the correct base template:
.. code-block:: mako
<%inherit file="/master/view.mako" />
<%def name="page_content()">
<p>THIS APPEARS FIRST!</p>
## nb. the form will appear here
${parent.page_content()}
<p>MADE IT TO THE END!</p>
</%def>

View file

@ -0,0 +1,10 @@
Templates
=========
.. toctree::
:maxdepth: 2
overview
base
lookup

View file

@ -0,0 +1,69 @@
Template Lookup
===============
The discovery of templates is handled by Mako, and is configurable.
WuttaWeb comes with all templates it needs, in the path designated as
``wuttaweb:templates``.
When the app renders a page, it invokes the Mako lookup logic, which
searches one or more folders and returns the first matching file it
encounters. By default ``wuttaweb:templates`` is the only place it
looks.
A template is searched for by "name" but it is more path-like, e.g.
``/page.mako`` or ``/master/index.mako`` etc. So for example the file
at ``wuttaweb:templates/home.mako`` is used for home page (using
lookup name ``/home.mako``) by default.
.. _mako-template-override:
Overriding the Search Paths
~~~~~~~~~~~~~~~~~~~~~~~~~~~
The basic idea is to give it a list of paths it should search when
trying to find a template. The first template file found for a given
search name is used and no further search is done for that name.
You can define the Mako lookup sequence in your ``web.conf`` as
follows:
.. code-block:: ini
[app:main]
mako.directories =
/random/path/on/disk
poser.web:templates
wuttaweb:templates
This setting is interpreted by ``pyramid_mako`` (`docs`_).
.. _docs: https://docs.pylonsproject.org/projects/pyramid_mako/en/latest/index.html#mako-directories
Here ``wuttaweb:templates/home.mako`` would still be used by default
for home page, *unless* e.g. ``/random/path/on/disk/home.mako``
existed in which case that would be used.
Each path can have an arbitrary set of templates, they will
effectively be combined to a single set by the app, with the
definition order determining search priority.
If you are already using a custom ``app.main()`` function for
constructing the web app during startup, it may be a good idea to
change the *default* search paths to include your package.
Setup for custom ``app.main()`` is beyond the scope here, but assuming
you *do* already have one, this is what it looks like::
from wuttaweb import app as base
def main(global_config, **settings):
# nb. set the *default* mako search paths; however config can
# still override with method shown above
settings.setdefault('mako.directories', ['poser.web:templates',
'wuttaweb:templates'])
return base.main(global_config, **settings)

View file

@ -0,0 +1,15 @@
Overview
========
WuttaWeb uses the `Mako`_ template language for page rendering.
.. _Mako: https://www.makotemplates.org/
There is a "global" base template which effectively defines the
"theme" (page layout, Vue component structure). A few other base
templates provide a starting point for any custom pages; see
:doc:`base`.
Templates are found via lookup which is handled by Mako. This is
configurable so you can override any or all; see :doc:`lookup`.

View file

@ -140,8 +140,8 @@
</div> </div>
</%def> </%def>
<%def name="modify_this_page_vars()"> <%def name="modify_vue_vars()">
${parent.modify_this_page_vars()} ${parent.modify_vue_vars()}
<script> <script>
ThisPageData.weblibs = ${json.dumps(weblibs or [])|n} ThisPageData.weblibs = ${json.dumps(weblibs or [])|n}

View file

@ -48,8 +48,8 @@
</%def> </%def>
<%def name="modify_this_page_vars()"> <%def name="modify_vue_vars()">
${parent.modify_this_page_vars()} ${parent.modify_vue_vars()}
<script> <script>
ThisPageData.configFiles = ${json.dumps([dict(path=p, priority=i) for i, p in enumerate(config.get_prioritized_files(), 1)])|n} ThisPageData.configFiles = ${json.dumps([dict(path=p, priority=i) for i, p in enumerate(config.get_prioritized_files(), 1)])|n}
</script> </script>

View file

@ -19,7 +19,8 @@
</div> </div>
</%def> </%def>
<%def name="modify_this_page_vars()"> <%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script> <script>
${form.vue_component}Data.usernameInput = null ${form.vue_component}Data.usernameInput = null

View file

@ -19,8 +19,10 @@
${self.body()} ${self.body()}
## Vue app ## Vue app
${self.make_whole_page_component()} ${self.render_vue_templates()}
${self.make_whole_page_app()} ${self.modify_vue_vars()}
${self.make_vue_components()}
${self.make_vue_app()}
</body> </body>
</html> </html>
@ -140,7 +142,7 @@
<%def name="head_tags()"></%def> <%def name="head_tags()"></%def>
<%def name="render_whole_page_template()"> <%def name="render_vue_template_whole_page()">
<script type="text/x-template" id="whole-page-template"> <script type="text/x-template" id="whole-page-template">
<div id="whole-page" <div id="whole-page"
style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;"> style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;">
@ -320,6 +322,63 @@
</script> </script>
</%def> </%def>
<%def name="render_vue_script_whole_page()">
<script>
let WholePage = {
template: '#whole-page-template',
computed: {},
mounted() {
for (let hook of this.mountedHooks) {
hook(this)
}
},
methods: {
changeContentTitle(newTitle) {
this.contentTitleHTML = newTitle
},
toggleNestedMenu(hash) {
const key = 'menu_' + hash + '_shown'
this[key] = !this[key]
},
% if request.is_admin:
startBeingRoot() {
this.$refs.startBeingRootForm.submit()
},
stopBeingRoot() {
this.$refs.stopBeingRootForm.submit()
},
% endif
},
}
let WholePageData = {
contentTitleHTML: ${json.dumps(capture(self.content_title))|n},
mountedHooks: [],
}
## declare nested menu visibility toggle flags
% for topitem in menus:
% if topitem['is_menu']:
% for item in topitem['items']:
% if item['is_menu']:
WholePageData.menu_${id(item)}_shown = false
% endif
% endfor
% endif
% endfor
</script>
</%def>
<%def name="render_this_page_component()"> <%def name="render_this_page_component()">
<this-page @change-content-title="changeContentTitle" /> <this-page @change-content-title="changeContentTitle" />
</%def> </%def>
@ -409,88 +468,25 @@
<%def name="render_prevnext_header_buttons()"></%def> <%def name="render_prevnext_header_buttons()"></%def>
<%def name="declare_whole_page_vars()"> <%def name="render_vue_templates()">
<script> ${self.render_vue_template_whole_page()}
${self.render_vue_script_whole_page()}
let WholePage = {
template: '#whole-page-template',
computed: {},
mounted() {
for (let hook of this.mountedHooks) {
hook(this)
}
},
methods: {
changeContentTitle(newTitle) {
this.contentTitleHTML = newTitle
},
toggleNestedMenu(hash) {
const key = 'menu_' + hash + '_shown'
this[key] = !this[key]
},
% if request.is_admin:
startBeingRoot() {
this.$refs.startBeingRootForm.submit()
},
stopBeingRoot() {
this.$refs.stopBeingRootForm.submit()
},
% endif
},
}
let WholePageData = {
contentTitleHTML: ${json.dumps(capture(self.content_title))|n},
mountedHooks: [],
}
## declare nested menu visibility toggle flags
% for topitem in menus:
% if topitem['is_menu']:
% for item in topitem['items']:
% if item['is_menu']:
WholePageData.menu_${id(item)}_shown = false
% endif
% endfor
% endif
% endfor
</script>
</%def> </%def>
<%def name="modify_whole_page_vars()"></%def> <%def name="modify_vue_vars()"></%def>
<%def name="finalize_whole_page_vars()"></%def> <%def name="make_vue_components()">
<%def name="make_whole_page_component()">
${make_wutta_components()} ${make_wutta_components()}
${self.render_whole_page_template()}
${self.declare_whole_page_vars()}
${self.modify_whole_page_vars()}
${self.finalize_whole_page_vars()}
<script> <script>
WholePage.data = function() { return WholePageData } WholePage.data = function() { return WholePageData }
Vue.component('whole-page', WholePage) Vue.component('whole-page', WholePage)
</script> </script>
</%def> </%def>
<%def name="make_whole_page_app()"> <%def name="make_vue_app()">
<script> <script>
new Vue({ new Vue({
el: '#app' el: '#app'
}) })
</script> </script>
</%def> </%def>

View file

@ -1,8 +1,6 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%def name="app_title()">${app.get_title()}</%def> <%def name="global_title()">${app.get_title()}</%def>
<%def name="global_title()">${self.app_title()}</%def>
<%def name="extra_styles()"></%def> <%def name="extra_styles()"></%def>

View file

@ -134,8 +134,8 @@
</b-notification> </b-notification>
</%def> </%def>
<%def name="modify_this_page_vars()"> <%def name="modify_vue_vars()">
${parent.modify_this_page_vars()} ${parent.modify_vue_vars()}
<script> <script>
% if simple_settings is not Undefined: % if simple_settings is not Undefined:

View file

@ -9,19 +9,16 @@
</div> </div>
</%def> </%def>
<%def name="render_this_page_template()"> <%def name="render_vue_templates()">
${parent.render_this_page_template()} ${parent.render_vue_templates()}
% if form is not Undefined: % if form is not Undefined:
${form.render_vue_template()} ${form.render_vue_template()}
% endif % endif
</%def> </%def>
<%def name="finalize_this_page_vars()"> <%def name="make_vue_components()">
${parent.finalize_this_page_vars()} ${parent.make_vue_components()}
% if form is not Undefined: % if form is not Undefined:
${form.render_vue_finalize()} ${form.render_vue_finalize()}
% endif % endif
</%def> </%def>
${parent.body()}

View file

@ -12,18 +12,16 @@
% endif % endif
</%def> </%def>
<%def name="render_this_page_template()"> <%def name="render_vue_templates()">
${parent.render_this_page_template()} ${parent.render_vue_templates()}
% if grid is not Undefined: % if grid is not Undefined:
${grid.render_vue_template()} ${grid.render_vue_template()}
% endif % endif
</%def> </%def>
<%def name="finalize_this_page_vars()"> <%def name="make_vue_components()">
${parent.finalize_this_page_vars()} ${parent.make_vue_components()}
% if grid is not Undefined: % if grid is not Undefined:
${grid.render_vue_finalize()} ${grid.render_vue_finalize()}
% endif % endif
</%def> </%def>
${parent.body()}

View file

@ -1,45 +1,25 @@
## -*- coding: utf-8; -*- ## -*- coding: utf-8; -*-
<%inherit file="/base.mako" /> <%inherit file="/base.mako" />
<%def name="context_menu_items()">
% if context_menu_list_items is not Undefined:
% for item in context_menu_list_items:
<li>${item}</li>
% endfor
% endif
</%def>
<%def name="page_content()"></%def> <%def name="page_content()"></%def>
<%def name="render_this_page()"> <%def name="render_vue_templates()">
<div style="display: flex;"> ${parent.render_vue_templates()}
${self.render_vue_template_this_page()}
<div class="this-page-content" style="flex-grow: 1;">
${self.page_content()}
</div>
<ul id="context-menu">
${self.context_menu_items()}
</ul>
</div>
</%def> </%def>
<%def name="render_this_page_template()"> <%def name="render_vue_template_this_page()">
<script type="text/x-template" id="this-page-template"> <script type="text/x-template" id="this-page-template">
<div style="height: 100%;"> <div style="height: 100%;">
${self.render_this_page()} ${self.page_content()}
</div> </div>
</script> </script>
</%def> <script>
<%def name="declare_this_page_vars()"> const ThisPage = {
<script type="text/javascript">
let ThisPage = {
template: '#this-page-template', template: '#this-page-template',
props: { props: {
configureFieldsHelp: Boolean, ## configureFieldsHelp: Boolean,
}, },
computed: {}, computed: {},
watch: {}, watch: {},
@ -51,31 +31,16 @@
}, },
} }
let ThisPageData = { const ThisPageData = {}
}
</script> </script>
</%def> </%def>
<%def name="modify_this_page_vars()"></%def> <%def name="make_vue_components()">
${parent.make_vue_components()}
<%def name="finalize_this_page_vars()"></%def> <script>
<%def name="make_this_page_component()">
${self.declare_this_page_vars()}
${self.modify_this_page_vars()}
${self.finalize_this_page_vars()}
<script type="text/javascript">
ThisPage.data = function() { return ThisPageData } ThisPage.data = function() { return ThisPageData }
Vue.component('this-page', ThisPage) Vue.component('this-page', ThisPage)
## <% request.register_component('this-page', 'ThisPage') %> ## <% request.register_component('this-page', 'ThisPage') %>
</script> </script>
</%def> </%def>
${self.render_this_page_template()}
${self.make_this_page_component()}