XF 2.3 What's new for developers in XenForo 2.3?

hys_9_cover.png
As promised, this week we're going to take a quick look at some of the more developer-centric changes coming in XenForo 2.3.

If a certain topic interests you more than others, click one of the links below:
Please note that the following libraries are no longer bundled with XF 2.3:
  • ccampbell/chromephp
  • doctrine/cache
  • league/flysystem-ziparchive
  • swiftmailer/swiftmailer
  • symfony/debug
  • web-token/jwt-signature-algorithm-eddsa
  • web-token/jwt-signature-algorithm-hmac
  • web-token/jwt-signature-algorithm-none
  • web-token/jwt-signature-algorithm-rsa

While we do have a little more to show you, the next couple of weeks is going to be focused on getting XenForo 2.3 ready to be installed here and some additional "Have you seen...?" posts may arrive between then and a public beta release. Until then, thanks for coming on this journey with us.
 

JavaScript​

In our second HYS we announced our decision to move away from jQuery and some of you are already releasing updates to your add-ons which is great to see. The following serves as a non-exhaustive reference for any specific framework changes that may affect how you write JavaScript code in your add-ons going forwards.

XF.extendObject

This is a new method which replaces jQuery's default $.extend() method. It works basically exactly the same, including the option to do a "deep" clone.

XF.createElementFromString

jQuery supported creating a new element with various properties and attributes entirely from a string, for example:

JavaScript:
const $input = $('<input type="text" readonly class="input" />')

We wanted to have something similar to this so we added a method which works similarly:

JavaScript:
const input = XF.createElementFromString('<input type="text" readonly class="input" />')

We have an entirely new concept too called XF.createElement which you can read about in a subsequent post.

Event management​

Some of the event management stuff in jQuery is pretty cool so we've replicated it as much as possible. Notably, we support namespaced events in a similar way to jQuery, along with equivalent methods to jQuery named XF.on(), XF.off(), XF.trigger() and XF.customEvent(). To handle delegated events we have a new XF.onDelegated method. If you previously used jQuery's one method so that an event listener is removed after it is first fired, you can now just pass { once: true } into your XF.on() calls.

Changes to XF.ajax

While the usage of XF.ajax() is mostly unchanged, clearly we no longer use jQuery's $.ajax() under the hood, which is a wrapper around XMLHttpRequest. We decided to move away from XMLHttpRequest in favour of the more modern Fetch API.

XF.ajax now returns a Promise which is similar to what is returned by jQuery, albeit with the promise method names being slightly different. Which were mentioned in the original have you seen post.

The other notable change though is how AJAX requests are aborted if needed. Previously, the object returned by jQuery had an abort method that could be called. The Fetch API has a different way of achieving this, which is a little more convoluted so we have created a new XF.ajaxAbortable method which makes this a little easier to work with, but it's worth noting that your existing usage of XF.ajax where a call may need to be aborted, will need to be changed.

Here's a usage example from form.js:

JavaScript:
const {
    ajax,
    abortController,
} = XF.ajaxAbortable('post', this.options.descUrl, { id: value }, this.onLoad.bind(this))

if (abortController)
{
    this.abortController = abortController
}

// ... elsewhere in the code

if (this.abortController)
{
    this.abortController.abort()
    this.abortController = null
}

XF.proxy

The XF.proxy method is typically used when you want to change the context of the this variable when calling another function. For example, if you pass a function as a callback when listening to a image load event (or similar), this in that callback would usually be a reference to the image itself. That's not usually desirable, so XF.proxy helps us keep the this context consistent.

While, of course, XF.proxy still exists and remains unchanged, please consider this deprecated and marked for removal in the future.

Instead, we now recommend and use the native JavaScript approach for this. This looks like:

JavaScript:
XF.on(form, 'reset', this.formReset.bind(this))

Primarily, this should help reduce errors and help navigating code in your IDE.

JavaScript animations and CSS-based transitions​

jQuery has a number of animation functions which we felt were worth keeping so we have rewritten them. The home for these new methods is under a new XF.Animate namespace and includes various approaches for sliding/fading content.

Here's an example where we fade up an existing container to hide it, replace its contents and then fade it back down to show the new content:

JavaScript:
XF.Animate.fadeUp(containerEl, {
    speed: XF.config.speed.fast,
    complete ()
    {
        containerEl.innerHTML = html.innerHTML
        XF.Animate.fadeDown(containerEl)
    },
})

You may also be familiar with our custom addClassTransitioned and removeClassTransitioned methods. Previously these were added as jQuery extensions. They have now been moved to a new XF.Transition namespace and require an element to be passed in as the first argument.

JavaScript:
XF.Transition.addClassTransitioned(this.errorElement, 'is-active')

Vendor library changes​

This section summarises the changes to vendor libraries which may impact your current add-ons.

Select2​

Unfortunately, Select2 is still written with jQuery as a dependency so it will no longer be included starting with XenForo 2.3. We only use Select2 for our "token input" system which is used as a tagging input and for multiple user selection (such as conversations). To keep this functionality we are now including a library called Tagify.

This pretty much has the same functionality you're used to but, ooh, look, avatars for multiple user input:

1700140920384.png



QR code generation​

We include a QR code generation library mostly for aiding the setup of TOTP as a two-factor authentication method. The previous version of this library relied on jQuery but newer versions have been rewritten without any specific dependencies. If you're using QR codes in any of your add-ons, this is just something you'll want to be aware of. The specific version of this library we're now using can be found here.


Star ratings​

Star ratings as you can see in both XenForo Media Gallery and XenForo Resource Manager previously relied on a third party library. A direct replacement didn't really exist so we just converted it ourselves to JavaScript. This is now a new class named XF.BarRating which you can find in rating.js.
 

SwiftMailer to Symfony Mail​

The announcement relating to the end of life of SwiftMailer was announced in 2021 and we quickly implemented Symfony Mail as a replacement... all the way back in 2021. While we could have rolled this out during the life of XenForo 2.2, there are a few small BC breaks.

Any class extensions to XF\Mail\Mail and XF\Mail\Mailer will likely be affected and will need changes to work with XenForo 2.3. This will mostly affect any methods which currently receive any object with a class that has a Swift_ prefix and the usage of those objects which may have a different API.

On the whole, Symfony Mail is essentially a ground up rewrite of SwiftMailer so the process of converting code to use Symfony Mail shouldn't cause too many problems.
 

Doctrine Cache to Symfony Cache​

Doctrine Cache has also been deprecated, and was replaced with the Symfony Cache component. While this change is similar in scope to Symfony Mailer, it has a greater chance of breaking sites which use add-ons that touch caching in some form.

With that in mind, we have provided a compatibility layer to reduce the likelihood of significant issues. When fetching a cache adapter from the service container, it is wrapped by an object with the same FQCN and interface as a Doctrine cache provider. In most cases, this should allow add-ons to continue working without changes. We use the Doctrine Cache wrapper in \XF\CssRenderer to retain backwards compatibility. Other classes which do not use our extension system have been updated to use the Symfony Cache adapter directly.

To facilitate migrating to the Symfony Cache adapter, you may retrieve it with a new boolean third argument to the \XF\App::cache method:

PHP:
$cache = \XF::app()->cache('context', true, false);

// setting a cache item
$item = $cache->getItem($id);
$item->set($data);
$item->expiresAfter($ttl);
$cache->save($item);

// retrieving a cache item
$data = $cache->getItem($id);

You can check out the Symfony Cache documentation for further details.
 

Updated Emogrifier​

We have updated the included Emogrifier library. This is the library which converts CSS classes used in email templates to inline styles to improve mail rendering compatibility across a range of email clients.

This has resulted in a slight change to the XF\Mail\Styler class where if you have added a class extension here which extends the __construct method specifically, you will need to make changes as it no longer receives an inliner property:

PHP:
public function __construct(CssRenderer $renderer)
 

Registry preload event​

Sometimes, certain data stored in the registry is required for most or potentially all requests. When a XenForo installation is not configured to use caching, loading data from the registry incurs a database query. In the core, we are able to preload common data in a single query when the framework is initialized, but this functionality was not available to add-ons because add-on data is, itself, stored in the registry. Catch-22! (Or cache-22...?)

We have introduced an app_preload_extra code event in XF 2.3, allowing add-ons to preload data from the registry. This means that all add-ons can load data from the registry together in a single query, instead of querying for data individually. The \XF\App object is also passed so that keys can be scoped to a specific application:

PHP:
public static function appPreloadExtra(\XF\App $app, array &$keys): void
{
    if ($app instanceof \XF\Pub\App)
    {
        $keys[] = 'myRegistryKey';
    }
}
 

Native IP conversion methods​

In previous versions, we used our own methods to convert IP addresses between text and binary. In XF 2.3, we have deprecated the \XF\Util\Ip::convertIpStringToBinary and \XF\Util\Ip::convertIpBinaryToString methods in favor of new \XF\Util\Ip::stringToBinary and \XF\Util\Ip::binaryToString methods. The new methods make use of the built-in PHP inet_pton and inet_ntop functions, which are more reliable and do not result in potentially unsafe ambiguities.

If you use the deprecated methods in an add-on, the new methods should function as a drop-in replacement in most cases. However, there are a few caveats to be aware of. The new methods will not attempt to handle situations where the IP address has already been converted to the target format, and will throw an exception if passed an invalid IP address by default. You may pass false as the last argument to return false instead of throwing an exception. The \XF\Util\Ip::binaryToString method also retains support for expanding IPv6 addresses where desirable.
 

Support for class strings​

Since XF 2.0, we have made extensive use of class "short names" for expanding class names and associating related objects, like entities and finders. While convenient, many extremely useful modern tools do not understand this convention, necessitating work-arounds like PhpStorm advanced metadata generation or custom extensions for static analysis tools.

In XF 2.3, we now support using fully qualified class names everywhere short names are supported. This includes fetching entities, finders, repositories, and other objects, as well as defining metadata like entity relations and behaviors. When using fully qualified class names, IDEs are better able to handle refactorings like class renames, and static analysers are better able to determine which objects are returned from container and factory methods.

PHP:
use XF\Entity\User;

// ...

$user = \XF::app()->find(User::class, 1);

// ...

$structure->relations = [
    'User' => [
        'entity' => User::class,
        'type' => self::TO_ONE,
        'conditions' => 'user_id',
        'primary' => true,
    ],
];
 

Updates to <time> tags - short date format​


While developing what has now become the XenForo 3 style, we made some enhancements to the HTML output of <time> tags (which are rendered when employing the <xf:date> tag in XenForo template syntax).

Although the feature is not actively in use in 2.3, it does not change the appearance of the template output, so rather than backing out the changes, we've left them in this version.

So what does it actually do?

In XenForo 2.2, we output time tags like this:
HTML:
<time datetime="2023-11-15T09:49:23+0000" data-time="1700041763"
   data-date-string="Nov 15, 2023" data-time-string="9:49 AM"
   title="Nov 15, 2023 at 9:49 AM">51 mins</time>
This is then updated with Javascript to maintain the time relative to the present.

For the new style, we want the ability to output a short date string when space is limited, so 51 minutes ago would be output as 51m. To do this, we include a new data-short attribute containing the short date/time format.
HTML:
<!-- 2m (2 minutes ago) -->
<time datetime="2023-11-15T10:43:14+0000" data-timestamp="1700044994"
    data-date="Nov 15, 2023" data-time="10:43 AM"
    data-short="2m" title="Nov 15, 2023 at 10:43 AM">2 minutes ago</time>
The same Javascript that updates the main output also updates the data-short attribute with the current short format.

Other examples:
HTML:
<!-- 13h (13 hours ago) -->
<time datetime="2023-11-14T21:00:23+0000" data-timestamp="1699995623"
    data-date="Nov 14, 2023" data-time="9:00 PM"
    data-short="13h" title="Nov 14, 2023 at 9:00 PM">Yesterday at 9:00 PM</time>

<!-- 2d (2 days ago) -->
<time datetime="2023-11-12T22:46:15+0000" data-timestamp="1699829175"
    data-date="Nov 12, 2023" data-time="10:46 PM"
    data-short="2d" title="Nov 12, 2023 at 10:46 PM">Sunday at 10:46 PM</time>

<!-- Sep 14 (September 14 this year) -->
<time datetime="2023-09-14T14:50:24+0100" data-timestamp="1694699424"
    data-date="Sep 14, 2023" data-time="2:50 PM"
    data-short="Sep 14" title="Sep 14, 2023 at 2:50 PM">Sep 14, 2023</time>

<!-- Nov '18 (November 12 2018) -->
<time datetime="2018-11-12T15:26:22+0000" data-timestamp="1542036382"
    data-date="Nov 12, 2018" data-time="3:26 PM"
    data-short="Nov '18" title="Nov 12, 2018 at 3:26 PM">Nov 12, 2018</time>

Naturally, these short date formats integrate with the language and phrase system, so if 2m doesn't mean 2 minutes in your language, you are free to edit the output in the same way as you can for the long date format.

By default, when showing a date from a year before the current, the short format excludes the day of the month (Nov '22 M 'y rather than Nov 15, '22 M j, 'y), but if you want to include it, you can do so with a custom date format such as M j 'y.

1700052561436.png


So, while you won't see this actually in use in 2.3, we have left it in place so that enterprising CSS and JS designers may make use of it as they see fit.
 

Template names in HTML output​

Viewing a page of XenForo HTML output and trying to work out how it was built, when it may contain snippets of many different templates can be challenging.

Or at least, it was.

While we have previously output a data-template attribute in the <body> tag to help developers and designers identify the main content template in use, we have now expanded the system considerably.

Now, by enabling the Embed template names in HTML option from the Appearance options section, your HTML output will include data-template-name attributes that make it easy to identify the template responsible for the particular part of the page you are inspecting.

1700063151724.png


These attributes are shown only to authenticated administrators.

HTML:
<html data-template-name="PAGE_CONTAINER" id="XF" lang="en-US" dir="LTR" data-xf="2.3" data-app="public" ...>
HTML:
<div data-template-name="thread_view" class="block block--messages" ...>

These data-template-name attributes are not part of the templates themselves, but rather they are added by the final output renderer, so you don't need to worry about keeping them updated, or cluttering up your template editing experience.

In practice, data-template-name attributes are added to the outermost rendered HTML tag in each template. There are a handful of instances where there is content in a template prior to an HTML tag, in which case the renderer will output that content prior to the labelled tag, but it will still be far easier to identify the responsible template than before.

The value of the data-template-name attribute is not limited to template names alone. In cases where the output comes from a template macro, the value of the attribute will include both the name of the template and the name of the macro.

HTML:
<article data-template-name="post_macros::post" class="message message--post" ...>

Screenshot 2023-11-15 at 13.22.58.png


Note that when enabled, these data-template-name tags are only output to authenticated administrators, so you should not rely on them as CSS selectors, unlike the original body[data-template] selector that is safe to use.

We have used data-template-name rather than reusing data-template as used in the <body> tag to avoid any potential CSS collisions where on-page styling has been attached to the data-template attribute, although we will probably use data-template for this feature in XenForo 3, and move the data-template attribute from the <body> tag to the <html> tag with a more descriptive name like data-content-template or something like that...
 

AbstractCollection nth method​

In response to a recent suggestion, we have implemented the ability to easily return the nth item from an AbstractCollection.

The most common use case for this is going to be in extracting specifically indexed items from a collection of Entities returned from a query.

For example, if you query for a collection of posts and you want to access the third post from the returned collection, you could simply call ->nth(3).

PHP:
$posts = $finder->fetch();
$third = $posts->nth(3);
$fifteenth = $posts->nth(15);
 

Smart Javascript cache buster​

Isn't it annoying when developing javascript to have to force a hard refresh every time you make a change to your code?

We thought so too, so we have built a new smart cache buster, which activates when you are using the fullJs config.php option.

Without going into too much detail, every page load will contain the latest version of your javascript files without having to do a hard refresh, while maintaining the cache of any that have not changed since the last page load.

It's the little things...
 

XF.createElement

This isn't strictly a replacement for any jQuery functionality, but it shortcuts some rather tedious vanilla Javascript that is commonly used.

Consider the following Javascript:
JavaScript:
const el = document.createElement('input')
el.type = 'text'
el.name = 'title'
el.value = 'Hello, world'
parentEl.appendChild(el)
That's one el of lot of els being typed, right?

With 2.3 and XF.createElement(), we can now do the following:
JavaScript:
const el = XF.createElement('input', {
    type: 'text',
    name: 'title',
    value: 'Hello, world'
}, parentEl)
Here, in a single declaration, we create the input element, assign it a bunch of properties and append it to the parentEl. We don't even need to assign the return value to a variable if that's not required by the subsequent code.

Both the object of properties and the node to which to append the element are optional parameters, although if you are not going to append the element to the DOM, it would be useless to not assign the return value to a variable 🤔

The properties object also supports a single level of nesting, so you can also do something like the following:
JavaScript:
const el = XF.createElement('span', {
    className: 'spanny-spanny-moo-moo',
    title: 'This is a span',
    dataset: {
        foo: 1,
        bar: 2
    },
    style: {
        color: 'red',
        fontWeight: 'bold'
    }
}, parentEl)
There is also a special-case attributes property, values of which will be set using el.setAttribute(name, value) if you need to use it.
 

Template macro syntax changes​

If you edit templates using an IDE, you'll probably make use of the structure outline pane to quickly navigate between sections of the template. It really helps when the IDE gives you a useful summary of elements, and when it comes to XenForo template macros, that really hasn't been the case to date.

Here's a mostly-collapsed view of the XenForo 2.2 public PAGE_CONTAINER template in the PhpStorm Structure pane, and the Visual Studio Code Outline pane:

1700130466373.png
1700130596311.png


Pretty useless, right?

The reason for this is that these tree views ignore the name attribute on tags in their summary view. Don't get me started on how stupid this is, especially when the name attribute is of critical importance for elements such as <input>, but it is what it is.

1700130968423.png


We can't do anything about <input>, <select> or <textarea> but we can do something about <xf:macro> so that it has a more useful representation in the outline view.

<xf:macro name="m" /> ➡ <xf:macro id="m" />​


It's a fairly simple change, and it just involves switching the name attribute for an id attribute. This can cause your IDE to complain about the same id tag being used on multiple elements, but this is safe to ignore, as those attributes will not be output to the final rendered HTML where it matters.

Let's switch the PAGE_CONTAINER template to use <xf:macro id...> in place of <xf:macro name...> and see how the outline view changes.

1700131395573.png
1700131480477.png


Isn't that better?

The name attribute is still available but deprecated, so you should get your templates updated.

<xf:macro template="t" name="m"> ➡ <xf:macro id="t::m" />​


We have also deprecated the <xf:macro template="template-name" name="macro-name" /> syntax, in favour of id="template-name::macro-name" format, which again assists with the ability to see a sensible outline of the structure.

1700132593967.webp
 

Direct messages​

We have mentioned this elsewhere but we have taken the decision to rename "Conversations" to "Direct messages". This terminology is more familiar to most internet users and has a better acronym "DM". Over the years often people have still called them "PM" for personal message or "PC" for personal conversations and this can be confusing for those that aren't familiar with the software.

In XF 2.3, this is entirely a visual change. All code references, class names and templates are still the same. There are new canonical routes named direct-messages and direct-messages/replies but all existing routes for conversations and conversations/messages will correctly redirect to the new routes. It is only phrases that have changed.

So there are no changes to make if you currently have add-ons that touch conversations and everything should still work. But we thought it worth clarifying that.

We may make more extensive changes in a future version and if that is the case we will provide plenty of warning.
 
Now, by enabling the Embed template names in HTML option from the Appearance options section, your HTML output will include data-template-name attributes that make it easy to identify the template responsible for the particular part of the page you are inspecting.
This is great news!
 
PHP:
$user = \XF::app()->find(User::class, 1);
Does this means find() (and anything else which takes a class string) is using template/generic type hints so $user will have the correct typehint for phpstorm and other static analysis tooling?
 
Does this means find() (and anything else which takes a class string) is using template/generic type hints so $user will have the correct typehint for phpstorm and other static analysis tooling?
In theory... But this doesn't currently work how I'd expect with PhpStorm unfortunately.

1700234148536.webp


Current PhpDoc looks like:

PHP:
/**
 * @template T of Entity
 *
 * @param string $shortName
 * @param mixed $id
 * @param array|string|null $with
 *
 * @return ($shortName is class-string<T> ? T|null : Entity|null)
 */

There might be scope for us to make some adjustments to this, or it might be the case that PhpStorm just doesn't support it yet.
 
Top Bottom