Element Library

Re: Element Library

by Sam Hemelryk -
Number of replies: 6
Hi Tim and Fred, thanks for the feedback.

I really would like to discuss this idea of renderables for everything.
I apologise in advance, this is going to be long. Perhaps the integrator in me coming out to describe fully what I want to convey.

As Fred mentioned at the start of his post, prototype B is appealing in that the "magic" lies documentation and understanding, the actual code is simple and familiar.
But there are hurdles for it.

Prototype A is object heavy, really too object heavy. But it tries to solve a couple of the hurdles faced by prototype B.
I wrote that approach and even I am not a fan of it, but it does the following things that I think are important and from it I've forms the following impression of the situation.

** disclaimer, I am discussing ideas, not specific elements. Please DO NOT tell me that element A should not be an element, I am not discussing what should and shouldn't be, but how generic things should and shouldn't be. Sad faces for anyone who chimes in to tell me that a search component is silly.

  1. When within a render method and a renderable "property" (coming from anywhere) is encountered the render method calls render on it. It never renderers it itself unless it is absolutely necessary.
    Why. Because when in my theme I override the render_button method I want to see it take effect on every button used in every component, and then if I need to change how a button looks for a component I want to target that component explicitly, overriding how it produces the button (more on that shortly).
    If you have a search component, and you've styled how buttons look in your theme of course you want the button in the component to inherit initially from your "standard" button.
    This implies the following:
    a. the button must be a renderable, in turn as button is just an example, anything you can design must be a renderable.
    It assumes:
    b. we want to re-use our design to achieve consistency. Starting at the smallest element we want to design up, if you've a component that needs an element to look unique the component has to do the work, and I would expect justify why it is unique (another sad face for the first person to say "but that doesn't fit with bootstrap")
  2. When rendering an element output context (not to be confused with Moodle contexts) should be given to provide background and flexibility on what is being rendered.
    I think this is a real devil, its very tempting when rendering a component that contains a button onto which you must set attributes that the component render method creates the HTML for the button with the requires HTML attributes.
    If you consider point 1 above a rule, then this approach would break it. By having the component render method produce the button your button no longer inherits the button design of the theme. It is not consistent through the render chain.
    To allow for this we give the render method context of what is being rendered.
    When rendering an element by itself there is no context given, when rendering the same element within a component the component gets indicated to the render method.

    That sounds a bit convoluted, and I myself misinterpreted the design of this in prototype A.
    This is how I achieved it in prototype A.
    There I introduced an output chain. When you called render on a component a "prerender" method was called, that populated a heirarchy for the component, so that each element contained within the component knew who it belonged to.
    That hierarchy was then used when render was called, the magic render method we already did looked for additional render methods that may or may not exist within the render chain.
    Using the menu as an example:
    • Their is a menu component
    • The menu component contains elements Menu > Element[]

    If you had a link that contained a button the following render methods would be looked for.
    • render_link_button
    • render_button (this you should have anyway, if element is a button this is render_button)

    Within core/a plugin you could define the first method if you had specific needs that were not already being met by the default method such as attributes.
    When overriding the renderer in a theme methods would be looked for in the following order.
    • theme renderer :: render_menu_element
    • theme renderer :: render_element
    • core renderer :: render_menu_element
    • core renderer :: render_element

    So you can see how defaults are respected here. When the theme says "this is how an element looks" that is the default that is then applied to the element in all of its output contexts.

    Worth mentioning is that I don't think that I got the approach quite correct in Prototype A.
    I've used the component in the method name to provide context, however thats not the complete context.
    The complete context would be to include the purpose the element serves as well somehow.
    So in the case of a complex component where more than one element of the same type were used for different things they would be distinguishable.
    It is already complex enough as it is of course.
  3. Components can impose properties onto elements that belong to them.
    This comes about again so that we adhere to point 1. We call render on everything. In point 2 you can see how render methods allow for output customisation to occur, the default is preferred for consistency but you can take control if you want.
    Point 2 however is very much limited to output. You can add attributes, change HTML etc. However there is also this notion of properties.
    Properties describe something about an element in relation to the component it is used within/
    A great example of this is that a menu node can be active, to describe the element in the menu as relating to the page the user is looking at.
    The element in the menu could be a number of different elements (link, button, search component etc) and this property should apply to all of them. However it only applied when those elements are used within this component.
    It's super duper important to differentiate active as a property from say a CSS class ".active".
    .active is how the active property may be described. But that won't apply to all frontend designs. Some designs may want to use a different class, or another attribute, or even a different tag.
    Prototype A facilitates this on the core_ui_element class, in which methods exist for adding, and setting properties.
  4. There is a finite number of elements and components
    This doesn't really fit as a rule, but I think its extremely important to mention.
    Don't get the idea into your head that you will be writing elements. You won't be. Elements are the basic building blocks they will be defined within core and they will change rarely. Written once, used often.
    Don't get the idea into your head that you will have to write components often. You won't. Components should be reusable. They should not be specific to what you are creating unless what you are creating is truly absolutely unique and new.
    We want to be able to create 99% of our interfaces with the elements and components in core.
    Even then you will need to write one component, maybe a couple if you're determined to make themers lives hard again.
    Tim I think I can hear you arguing this already. But really I don't believe it will happen but once in a blue moon.

Those points I consider to be important.

It's very important to note one other thing.
All of this magic in regards to which method gets used, I think these rules only apply core/plugins.
Themes, I would like to think that themes can do what they want.
This approach is about building a consistent system that they can use, and that favours minimal work as I see it in themes.
If they choose to override the render_menu and render absolutely everything in there I think that is entirely acceptable.
It is still up to the themer to craft the end result that they are looking for in the way they want.
But hopefully this magic provides them a way to do it easily, making the page easy to restyle.

I am sure that with strict rules and proper understanding we can apply these to prototype B.
We can require people to always call the render method, we can invent a way to publish theme defaults methods past the core more exclusive methods, and we can continue to employ dark magic (dom manipulation, I really dislike this, I wish there was a better solution).

Obviously we've got to make a decision, the decision we make is what we will live with.
There is more unknown with the OO approach, and for sure it is complex.
But I don't think it should be dismissed lightly.

I've tried to simplify the approach a bit and have come up with these two alternatives:

Ok enough for now.
Please please please give the OO approach some thought and let me know.
I myself an not sold on it, but I think we are dismissing it to lightly.

Cheers
Sam

Edit: We'll utilise MDL-45885 to discuss and document the fine details once this conversation on approach matures.
In reply to Sam Hemelryk

Re: Element Library

by Damyon Wiese -
Thanks Sam,

While I like the simplicity of approach B - I do not think the DOM manipulation is simple/scalable. Imagine a list with 1000 items - parsing the entire dom of the list to add an attribute to the top level node is wasteful. Neither do I think it can be solved with regexes or any other "quick fix" - I think it's doomed to fail.

I also thing that approach A was too complex. There was a lot of hidden detail in that branch, that I could not follow from reading the code alone.

Freds suggestion does sound good initially - but I agree that if you take that path down the road you will have to overide 20 render methods to change the style of a button consistently.

Really I like the further work you did - and the prototype_3 branch seems the closest to a nice solution I have seen. It is relatively simple, but allows parent containers to put some additional "state" into the renderables it contains. The method for rendering a (e.g.) button is defined only once - but it is flexible enough to cover the different uses.

There are some more fine details to be worked out there, such as the stripping of "core_ui_" prefixes or whether we want to use namespaces such as in https://tracker.moodle.org/browse/MDL-41663 to achieve this. (I don't think I am in favour of stripping the prefixes even if it means long render method names - at least the long names lead to easier finding of the exact method to overwrite - less magic).

So - ATM I give my vote to prototype_3 with a bit more finesse-ing.


In reply to Damyon Wiese

Re: Element Library

by Tim Hunt -
Picture of Core developers Picture of Documentation writers Picture of Particularly helpful Moodlers Picture of Peer reviewers Picture of Plugin developers

There is mention of various prototypes. Where?

I have more to say, but no time. Argh!

In reply to Sam Hemelryk

Re: Element Library

by Frédéric Massart ⭐ -
Picture of Core developers Picture of Plugin developers Picture of Testers

(Markdown seems to play tricks on my post, it seems more readable here)

Hi Sam,

thanks for your feedback. I have to say that I do not disagree with your approach, I agree with most of it. the only thing that I dislike is the developer that makes assumptions on how things should be displayed.

If you have a renderable that is called dropdown_renderable, then it is awkward for a designer to overwrite that to display it differently as the name does not make sense any more. For instance if I do not want a dropdown, but a horizontal list of links, or tabs, ...

What I am suggesting is that renderables are containers of information that are not tied to their display. If you look at it from a templating point of view, you would say that the renderables contain the variables to pass to the template, ignoring what the final display would be. This is common to a more stricter MVC model, and I guess we could apply it to Moodle.

If the developers hold the logic in the controller, gather from the model the information, and sends the data to the renderers, then themers have full control at the view level. That is what I would like us to achieve, leaving developers away from the design decisions and giving full control to themers. Of course developers will have to make a decision for the view implemented in core, but that does not stop anyone from rendering things differently.

We could talk about backwards compatibility too. If at some point we decide to change the layout of a page, we will have to update the controller to change the renderable to something else. As a themer, that means that the renderer I wrote for that renderer is not called in that location any more, and thus it breaks the layout I had in mind.

Let me try to explain with examples, and some code.

Actions not links

How are links supposed to be displayed? Well, commonly, they would be displayed as an anchor tag, with or without an icon. But they could be displayed differently in different locations. They could be real buttons attached to a form, or triggered with JavaScript, they could be just icons, they could be anchors styled as buttons, they are not necessarily links, to me those are actions.

By creating a link_renderable, the developers implies that it should be a link. While if you define it as an action_renderable, it is an action and it is displayed differently depending on its location.

Code example

Let's see how that could look in code, we will create renderables and renderers to display a basic forum post that has a few actions like 'reply' and 'delete'.

First we assume that core has an action_renderable, and a standard way to display it.

// Renderable.
class action_renderable implements renderable {
    $text = 'Something';
    $destination = 'http://somewhere';
    $icon = pixurlObject;
    $disabled = false;
    active = false;
}

// Default renderer.
public function render_action(action_renderable $renderable) {
    $text = $this->render($renderable->icon) . $renderable->text;
    return html_writer::link($renderable->destination, $text);
}

This is the very base, and it follows the different prototypes, now let's define a dropdown menu in core.

// Renderable of list of actions.
class actions_list_renderable implements renderable {
    $actions = array of action_renderable;
}

// Default renderer.
public function render_dropdown(actions_list_renderable $actions) {
    $html = '<div class="dropdown">';
    $html .= '<ul>';
    foreach ($actions as $action) {
        $html .= '<li>' . $this->render($action) . '</li>';
    }
    $html .= '</ul>';
    $html .= '</div>';
    return $html;
}

As you can see, the renderer for the dropdown menu does not specifically accept a dropdown_renderable, but a list of actions.

Now in my module, I would have something like this to output a forum post:

// Renderable for a post.
public function render_post(myown_renderable $renderable) {
    $html = '<div class="post">';
    $html .= '<div class="content">';
    $html .= $renderable->content;
    $html .= '</div>';
    $html .= '<div class="actions">';
    $html .= $this->render_dropdown($renderable->actions);
    $html .= '</div>';
    $html .= '</div>';
    return $html;
}

If we stop thinking further for the moment, we find the same benefits as for the other prototypes:

  • The designer can overwrite the default look of an action_renderable.
  • The designer can overwrite the default look of a dropdown.
  • The designer can change the look of the post

Now let's say that the themer does not want to have a dropdown in the post, they want a horizontal list of links. We have three options here.

1/ They use CSS specific to the module

They use CSS, target the specific pages they are interested in, and use the existing markup to layout the dropdown as a list. But there are two issues with this solution:

  • The CSS cannot be easily re-used throughout Moodle for other dropdowns that they want to see as lists.
  • The existing markup might not fit their styling needs (lack of divs, classes, ...).

2/ There is a core renderer for horizontal list of actions.

The designer will overwrite the default implementation of render_post, and call the render_horizontal_list_of_actions.

// Renderable for a post.
public function render_post(myown_renderable $renderable) {
    $html = '<div class="post">';
    $html .= '<div class="content">';
    $html .= $renderable->content;
    $html .= '</div>';
    $html .= '<div class="actions">';
    $html .= $this->render_horizontal_list_of_actions($renderable->actions);
    $html .= '</div>';
    $html .= '</div>';
    return $html;
}

3/ Core does not provide such a method.

And so they create their own and overwrite the default render_post() renderer.

// Renderer for a horizontal list.
public function render_horizontal_list_of_actions(actions_list_renderable $actions) {
    $html = '<div class="dropdown">';
    $html .= '<ul>';
    foreach ($actions as $action) {
        $html .= '<li>' . $this->render($action) . '</li>';
    }
    $html .= '</ul>';
    $html .= '</div>';
    return $html;
}

// Renderable for a post.
public function render_post(myown_renderable $renderable) {
    $html = '<div class="post">';
    $html .= '<div class="content">';
    $html .= $renderable->content;
    $html .= '</div>';
    $html .= '<div class="actions">';
    $html .= $this->render_horizontal_list_of_actions($renderable->actions);
    $html .= '</div>';
    $html .= '</div>';
    return $html;
}

Solution #2 is the easiest, and surely the encouraged one, but the solution #3 is a good enough alternative because it creates a re-usable component. It also means that if later on we update core not to display a dropdown but something else, my own design will not be affected as the renderable I am getting is identical.

Adapting to other frameworks

Now, if we think of the adaptation to other frameworks, and the need to change the dom structure and add classes and stuff, you will notice that the dropdown_render method is not quite right because it refers to render_action(). So we have to provide a way for designers to change the look of an action within the context of the dropdown.

We have two solutions here:

1/ Specifically create a new render_action_in_dropdown()

public function render_action_in_dropdown(action_renderable $renderable) {
    $text = $this->render($renderable->icon . $renderable->text);
    return html_writer::link($renderable->destination, $text);
}

public function render_dropdown(actions_list_renderable $actions) {
    $html = '<div class="dropdown">';
    $html .= '<ul>';
    foreach ($actions as $action) {
        $html .= '<li>' . $this->render_action_in_dropdown($action) . '</li>';
    }
    $html .= '</ul>';
    $html .= '</div>';
    return $html;
}

2/ The display-context is passed to render()

We could define more specific renderers attached to a display-context (or components), and have something like:

public function render_dropdown_action(action_renderable $renderable) {
    $text = $this->render($renderable->icon . $renderable->text);
    return html_writer::link($renderable->destination, $text);
}

public function render_dropdown(actions_list_renderable $actions) {
    $html = '<div class="dropdown">';
    $html .= '<ul>';
    foreach ($actions as $action) {
        $html .= '<li>' . $this->render($action, $context = 'dropdown') . '</li>';
    }
    $html .= '</ul>';
    $html .= '</div>';
    return $html;
}

The render method would be smart enough to first check for the existence of a render method called render_dropdown_action, and if it does not exist refer to the default one, render_action.

Of course, the context should be well defined and only used within the same context. It would unacceptable from render_dropdown() to call render($action, $context = 'tabs').

Maybe there is a magical way to guess the context we are in from the render() method without having to pass an argument.

Complex list of actions

In some dropdowns (see Bootstrap 3), you can include dividers, and headers. So we have to think about more complex list of actions that can contain not only action_renderable, but also divider_renderable and header_renderable.

Let's try.

// PHP logic.
$actions = array(
    new header_renderable('Some pages'),
    new action_renderable('http://', 'Page 1', $icon),
    new divider_renderable(),
    new action_renderable('http://', 'Page 2', $icon),
    new action_renderable('http://', 'Page 3', $icon),
    new divider_renderable(),
    new action_renderable('http://', 'Page 4', $icon),
);
$list = new actions_list_renderable($actions);
$post = new myown_renderable($foo, $actions);
echo $OUTPUT->render($post);

// Renderer for dropdown dividers.
public function render_dropdown_divider(divider_renderable $renderable) {
    return '<div class="divider"></div>';
}

// Renderer for dropdown headers.
public function render_dropdown_header(header_renderable $renderable) {
    return '<div class="header">' . $renderable->text . '</div>';
}

// Renderer for dropdowns.
public function render_dropdown(actions_list_renderable $actions) {
    $html = '<div class="dropdown">';
    $html .= '<ul>';
    foreach ($actions as $action) {
        $html .= '<li>' . $this->render($action, $context = 'dropdown') . '</li>';
    }
    $html .= '</ul>';
    $html .= '</div>';
    return $html;
}

Being able to pass/guess the context from the render method is really helful here, because we can automatically match the different types of objects contained in the actions_list_renderable.

And if core were not to provide a renderer for headers in dropdown, I could add it to my theme renderers when I adapt it to Boostrap 3 for instance.

Ideas of renderables

  • action_renderable: An action, represented as a link, a button, etc...
  • actions_list_renderable: A list of actions, rendererd as a dropdown, tabs, list of links, ...
  • user_renderable: Rendered as picture, picture + name, name
  • post_renderable: For comments, blogs, or forum posts. (time, author, content, actions)
  • block_renderable: For any kind of block of content that contains a header, content and footer (blocks, course sections, forum discussion page)

I am not really in favour of layout renderables (2 vertical columns, grid, etc...) because essentially the output should always be defined from a renderer, and I can overwrite that renderer to change 2-columns to 2-rows if I want. I do not see any benefit for a designer to change a 2-column component to 2-rows site-wide.

Food for thoughts

While writing this list, it came in my mind that maybe we would want to introduce a collection_renderable, that contains a list of objects, and call render on them with the 'collection' context, leading to:

  • render_collection_user
  • render_collection_block

Javascript

I did not look at all at the Javascript in your patches, but if the renderable is a container of information, without a context, you could simply extract its data, json'd it, and pass it to Javascript. Maybe it could also be easy to create handlebars templates from PHP by having ->render() to replace the final variables with {{ context:placeholder }} or something. I don't know, I haven't thought about this really.

Plugin renderers

For core and non-core, plugins should always define a renderer, even if it does not contain anything. Then they should always get their own renderer rather than $OUTPUT. If their renderer is empty it will fully rely on core, which is fine, but at least themers can override the output of a specific plugin if they need to.

Final thoughts

Briefly looking at your patch, I would be concerned about the difficulty for designers to understand the PHP logic when dealing with core_ui stuff. Calling add_class() or is() is fine for developers but not so much for designers. Though, I give you that it is a lot simpler than the existing renderers we have.

Also, I saw somewhere that we would have to deal with cloning objects to prevent random behaviours when modifying their values, I am definitely sure that designers should never ever have to think about that.

You and Damyon have done a great work on this already, and I am coming once the spec is already well written, and I apologize for that. I am surely missing some cases in the ideas developed above, but hopefully there is something to get out of it.

Thanks! Fred

In reply to Sam Hemelryk

Re: Element Library

by Tim Hunt -
Picture of Core developers Picture of Documentation writers Picture of Particularly helpful Moodlers Picture of Peer reviewers Picture of Plugin developers

Sorry, Sam, but I have to say that your post is almost incomprehensible. So many words. I have not idea what they are saying (probably not helped by the fact that I don't have enough time to read them properly, but still.)

Fred's post, with code snippets to make it clear what he means, is so much easier to follow.

In reply to Tim Hunt

Re: Element Library

by Damyon Wiese -
Freds post - while useful does not contain the detail of an entire branch of code with all the edge cases discovered - that is why me and Sam made branches of different solutions that produce exactly the same output as required by the 4 different frameworks.

All the prototype branches are listed here or on the spec page.

Repeating them:
A) https://github.com/samhemelryk/moodle/tree/output_prototype
B) https://github.com/damyon/moodle/tree/MENU_RENDERABLE
C) https://github.com/samhemelryk/moodle/tree/output_prototype_2
D) https://github.com/samhemelryk/moodle/tree/output_prototype_3

I am mulling over a proper (detailed) response to Freds suggestion which contains a mix of positives and negatives (just like the other suggestions). I think we have not arrived at a solution yet but we are edging closer.
In reply to Damyon Wiese

Re: Element Library

by Tim Hunt -
Picture of Core developers Picture of Documentation writers Picture of Particularly helpful Moodlers Picture of Peer reviewers Picture of Plugin developers

I know that Fred's post does not contain a complete working prototype of a whole system. Such prototypes are very useful, it is great that you have made them, and I need to find time to study them.

However, what I was trying to say is that small snippets of code, showing particular aspects of the system, really help the explanation. After all, the key things is how this new API looks to users. So, just like we make UI mock-ups in the tracker to quickly make it clear what were are talking about when designing a new bit of Moodle functionality, we need something similar here. API are the 'user-interface' that third-party developers user to interact with Moodle, and it should be possible to present a quick sketch of the key things like:

  • I am making a plugin, and I want to output a user picture.
  • I am a themed and I want to change how the user picture is displayed.
  • I am making a plugin, and I want to define a new thing like a forum post that can be displayed.
Sure, there also needs to be a lot of clever back-end code to make what is shown in the mock-ups work, but that is an implementation detail, and only a few people need to understand that. Many people will need to work with the public face of this in future, so that is why it is worth showing small snippets that mock-up how that will look.