Views

Introduction

Views are responsible for creating output. They handle everything from:

  • the layout of pages
  • chunks of presentation output (like a footer or a toolbar)
  • individual links and form inputs.
  • the images, js, and css needed by your web page

Using views

At their most basic level, the default views are just PHP files with snippets of html:

<h1>Hello, World!</h1>

Assuming this view is located at /views/default/hello.php, we could output it like so:

echo elgg_view('hello');

For your convenience, Elgg comes with quite a lot of views by default. In order to keep things manageable, they are organized into subdirectories. Elgg handles this situation quite nicely. For example, our simple view might live in /views/default/hello/world.php, in which case it would be called like so:

echo elgg_view('hello/world');

The name of the view simply reflects the location of the view in the views directory.

Views as templates

You can pass arbitrary data to a view via the $vars array. Our hello/world view might be modified to accept a variable like so:

<h1>Hello, <?= $vars['name']; ?>!</h1>

In this case, we can pass an arbitrary name parameter to the view like so:

echo elgg_view('hello/world', ['name' => 'World']);

which would produce the following output:

<h1>Hello, World!</h1>

Warning

Views don’t do any kind of automatic output sanitization by default. You are responsible for doing the correct sanitization yourself to prevent XSS attacks and the like.

Views as cacheable assets

As mentioned before, views can contain JS, CSS, or even images.

Asset views must meet certain requirements:

  • They must not take any $vars parameters
  • They must not change their output based on global state like
    • who is logged in
    • the time of day
  • They must contain a valid file extension
    • Bad: my/cool/template
    • Good: my/cool/template.html

For example, suppose you wanted to load some CSS on a page. You could define a view mystyles.css, which would look like so:

/* /views/default/mystyles.css */
.mystyles-foo {
  background: red;
}

Note

Leave off the trailing “.php” from the filename and Elgg will automatically recognize the view as cacheable.

To get a URL to this file, you would use elgg_get_simplecache_url:

// Returns "https://mysite.com/.../289124335/default/mystyles.css
elgg_get_simplecache_url('mystyles.css');

Elgg automatically adds the magic numbers you see there for cache-busting and sets long-term expires headers on the returned file.

Warning

Elgg may decide to change the location or structure of the returned URL in a future release for whatever reason, and the cache-busting numbers change every time you flush Elgg’s caches, so the exact URL is not stable by design.

With that in mind, here’s a couple anti-patterns to avoid:

  • Don’t rely on the exact structure/location of this URL
  • Don’t try to generate the URLs yourself
  • Don’t store the returned URLs in a database

In your plugin’s init function, register the css file:

elgg_register_css('mystyles', elgg_get_simplecache_url('mystyles.css'));

Then on the page you want to load the css, call:

elgg_load_css('mystyles');

Views and third-party assets

The best way to serve third-party assets is through views. However, instead of manually copy/pasting the assets into the right location in /views/*, you can map the assets into the views system via the "views" key in your plugin’s elgg-plugin.php config file.

The views value must be a 2 dimensional array. The first level maps a viewtype to a list of view mappings. The secondary lists map view names to file paths, either absolute or relative to the Elgg root directory.

If you check your assets into source control, point to them like this:

<?php // mod/example/elgg-plugin.php
return [
    // view mappings
    'views' => [
        // viewtype
        'default' => [
            // view => /path/from/filesystem/root
            'js/jquery-ui.js' => __DIR__ . '/bower_components/jquery-ui/jquery-ui.min.js',
        ],
    ],
];

To point to assets installed with fxp/composer-asset-plugin, use install-root-relative paths by leaving off the leading slash:

<?php // mod/example/elgg-plugin.php
return [
    'views' => [
        'default' => [
            // view => path/from/install/root
            'js/jquery-ui.js' => 'vendor/bower-asset/jquery-ui/jquery-ui.min.js',
        ],
    ],
];

Elgg core uses this feature extensively, though the value is returned directly from /engine/views.php.

Note

You don’t have to use Bower, Composer Asset Plugin, or any other script for managing your plugin’s assets, but we highly recommend using a package manager of some kind because it makes upgrading so much easier.

Specifying additional views directories

In elgg-plugin.php you can also specify directories to be scanned for views. Just provide a view name prefix ending with / and a directory path (like above).

<?php // mod/file/elgg-plugin.php
return [
    'views' => [
        'default' => [
            'file/icon/' => __DIR__ . '/graphics/icons',
        ],
    ],
];

With the above, files found within the icons folder will be interpreted as views. E.g. the view file/icon/general.gif will be created and mapped to mod/file/graphics/icons/general.gif.

Note

This is a fully recursive scan. All files found will be brought into the views system.

Multiple paths can share the same prefix, just give an array of paths:

<?php // mod/file/elgg-plugin.php
return [
    'views' => [
        'default' => [
            'file/icon/' => [
                __DIR__ . '/graphics/icons',
                __DIR__ . '/more_icons', // processed 2nd (may override)
            ],
        ],
    ],
];

Viewtypes

You might be wondering: “Why /views/default/hello/world.php instead of just /views/hello/world.php?”.

The subdirectory under /views determines the viewtype of the views below it. A viewtype generally corresponds to the output format of the views.

The default viewtype is assumed to be HTML and other static assets necessary to render a responsive web page in a desktop or mobile browser, but it could also be:

  • RSS
  • ATOM
  • JSON
  • Mobile-optimized HTML
  • TV-optimized HTML
  • Any number of other data formats

You can force Elgg to use a particular viewtype to render the page by setting the view input variable like so: https://mysite.com/?view=rss.

You could also write a plugin to set this automatically using the elgg_set_viewtype() function. For example, your plugin might detect that the page was accessed with an iPhone’s browser string, and set the viewtype to iphone by calling:

elgg_set_viewtype('iphone');

The plugin would presumably also supply a set of views optimized for those devices.

Altering views via plugins

Without modifying Elgg’s core, Elgg provides several ways to customize almost all output:

Overriding views

Views in plugin directories always override views in the core directory; however, when plugins override the views of other plugins, later plugins take precedent.

For example, if we wanted to customize the hello/world view to use an h2 instead of an h1, we could create a file at /mod/example/views/default/hello/world.php like this:

<h2>Hello, <?= $vars['name']; ?></h2>

Note

When considering long-term maintenance, overriding views in the core and bundled plugins has a cost: Upgrades may bring changes in views, and if you have overridden them, you will not get those changes.

You may instead want to alter the input or the output of the view via plugin hooks.

Note

Elgg caches view locations. This means that you should disable the system cache while developing with views. When you install the changes to a production environment you must flush the caches.

Extending views

There may be other situations in which you don’t want to override the whole view, you just want to prepend or append some more content to it. In Elgg this is called extending a view.

For example, instead of overriding the hello/world view, we could extend it like so:

elgg_extend_view('hello/world', 'hello/greeting');

If the contents of /views/default/hello/greeting.php is:

<h2>How are you today?</h2>

Then every time we call elgg_view('hello/world');, we’ll get:

<h1>Hello, World!</h1>
<h2>How are you today?</h2>

You can prepend views by passing a value to the 3rd parameter that is less than 500:

// appends 'hello/greeting' to every occurrence of 'hello/world'
elgg_extend_view('hello/world', 'hello/greeting');

// prepends 'hello/greeting' to every occurrence of 'hello/world'
elgg_extend_view('hello/world', 'hello/greeting', 450);

All view extensions should be registered in your plugin’s init,system event handler in start.php.

Altering view input

It may be useful to alter a view’s $vars array before the view is rendered.

Since 1.11, before each view rendering the $vars array is filtered by the plugin hook ["view_vars", $view_name]. Each registered handler function is passed these arguments:

  • $hook - the string "view_vars"
  • $view_name - the view name being rendered (the first argument passed to elgg_view())
  • $returnvalue - the modified $vars array
  • $params - an array containing:
    • vars - the original $vars array, unaltered
    • view - the view name
    • viewtype - The viewtype being rendered

Altering view input example

Here we’ll alter the default pagination limit for the comments view:

elgg_register_plugin_hook_handler('view_vars', 'page/elements/comments', 'myplugin_alter_comments_limit');

function myplugin_alter_comments_limit($hook, $type, $vars, $params) {
    // only 10 comments per page
    $vars['limit'] = elgg_extract('limit', $vars, 10);
    return $vars;
}

Altering view output

Sometimes it is preferable to alter the output of a view instead of overriding it.

The output of each view is run through the plugin hook ["view", $view_name] before being returned by elgg_view(). Each registered handler function is passed these arguments:

  • $hook - the string "view"
  • $view_name - the view name being rendered (the first argument passed to elgg_view())
  • $result - the modified output of the view
  • $params - an array containing:
    • viewtype - The viewtype being rendered

To alter the view output, the handler just needs to alter $returnvalue and return a new string.

Altering view output example

Here we’ll eliminate breadcrumbs that don’t have at least one link.

elgg_register_plugin_hook_handler('view', 'navigation/breadcrumbs', 'myplugin_alter_breadcrumb');

function myplugin_alter_breadcrumb($hook, $type, $returnvalue, $params) {
    // we only want to alter when viewtype is "default"
    if ($params['viewtype'] !== 'default') {
        return $returnvalue;
    }

    // output nothing if the content doesn't have a single link
    if (false === strpos($returnvalue, '<a ')) {
        return '';
    }

    // returning nothing means "don't alter the returnvalue"
}

Replacing view output completely

You can pre-set the view output by setting $vars['__view_output']. The value will be returned as a string. View extensions will not be used and the view hook will not be triggered.

elgg_register_plugin_hook_handler('view_vars', 'navigation/breadcrumbs', 'myplugin_no_page_breadcrumbs');

function myplugin_no_page_breadcrumbs($hook, $type, $vars, $params) {
    if (elgg_in_context('pages')) {
        return ['__view_output' => ""];
    }
}

Displaying entities

If you don’t know what an entity is, check this page out first.

The following code will automatically display the entity in $entity:

echo elgg_view_entity($entity);

As you’ll know from the data model introduction, all entities have a type (object, site, user or group), and optionally a subtype (which could be anything - ‘blog’, ‘forumpost’, ‘banana’).

elgg_view_entity will automatically look for a view called type/subtype; if there’s no subtype, it will look for type/type. Failing that, before it gives up completely it tries type/default.

RSS feeds in Elgg generally work by outputting the object/default view in the ‘rss’ viewtype.

For example, the view to display a blog post might be object/blog. The view to display a user is user/default.

Full and partial entity views

elgg_view_entity actually has a number of parameters, although only the very first one is required. The first three are:

  • $entity - The entity to display
  • $viewtype - The viewtype to display in (defaults to the one we’re currently in, but it can be forced - eg to display a snippet of RSS within an HTML page)
  • $full_view - Whether to display a full version of the entity. (Defaults to false.)

This last parameter is passed to the view as $vars['full_view']. It’s up to you what you do with it; the usual behaviour is to only display comments and similar information if this is set to true.

Listing entities

This is then used in the provided listing functions. To automatically display a list of blog posts (see the full tutorial), you can call:

echo elgg_list_entities([
    'type' => 'object',
    'subtype' => 'blog',
]);

This function checks to see if there are any entities; if there are, it first displays the navigation/pagination view in order to display a way to move from page to page. It then repeatedly calls elgg_view_entity on each entity before returning the result.

Note that elgg_list_entities allows the URL to set its limit and offset options, so set those explicitly if you need particular values (e.g. if you’re not using it for pagination).

Elgg knows that it can automatically supply an RSS feed on pages that use elgg_list_entities. It initializes the ["head","page"] plugin hook (which is used by the header) in order to provide RSS autodiscovery, which is why you can see the orange RSS icon on those pages in some browsers.

If your entity list will display the entity owners, you can improve performance a bit by preloading all owner entities:

echo elgg_list_entities([
    'type' => 'object',
    'subtype' => 'blog',

    // enable owner preloading
    'preload_owners' => true,
]);

See also this background information on Elgg’s database.

If you want to show a message when the list does not contain items to list, you can pass a no_results message. If you want even more controle over the no_results message you can also pass a Closure (an anonymous function).

echo elgg_list_entities([
    'type' => 'object',
    'subtype' => 'blog',

    'no_results' => elgg_echo('notfound'),
]);

Rendering a list with an alternate view

Since 1.11, you can define an alternative view to render list items using 'item_view' parameter.

In some cases, default entity views may be unsuitable for your needs. Using item_view allows you to customize the look, while preserving pagination, list’s HTML markup etc.

Consider these two examples:

echo elgg_list_entities_from_relationship([
    'type' => 'group',
    'relationship' => 'member',
    'relationship_guid' => elgg_get_logged_in_user_guid(),
    'inverse_relationship' => false,
    'full_view' => false,
]);
echo elgg_list_entities_from_relationship([
    'type' => 'group',
    'relationship' => 'invited',
    'relationship_guid' => (int) $user_guid,
    'inverse_relationship' => true,
    'item_view' => 'group/format/invitationrequest',
]);

In the first example, we are displaying a list of groups a user is a member of using the default group view. In the second example, we want to display a list of groups the user was invited to.

Since invitations are not entities, they do not have their own views and can not be listed using elgg_list_*. We are providing an alternative item view, that will use the group entity to display an invitation that contains a group name and buttons to access or reject the invitation.

Rendering a list as a table

Since 2.3 you can render lists as tables. Set $options['list_type'] = 'table' and provide an array of TableColumn objects as $options['columns']. The service elgg()->table_columns provides several methods to create column objects based around existing views (like page/components/column/*), properties, or methods.

In this example, we list the latest my_plugin objects in a table of 3 columns: entity icon, the display name, and a friendly format of the time.

echo elgg_list_entities([
    'type' => 'object',
    'subtype' => 'my_plugin',

    'list_type' => 'table',
    'columns' => [
        elgg()->table_columns->icon(),
        elgg()->table_columns->getDisplayName(),
        elgg()->table_columns->time_created(null, [
            'format' => 'friendly',
        ]),
    ],
]);

See the Elgg\Views\TableColumn\ColumnFactory class for more details on how columns are specified and rendered. You can add or override methods of elgg()->table_columns in a variety of ways, based on views, properties/methods on the items, or given functions.