Element Library

Re: Element Library

by Frédéric Massart ⭐ -
Number of replies: 0
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