Sven Ruppert

Dynamic PageTitle in Vaadin Flow

This tutorial describes various ways in which you can set the page title in Flow at runtime, focusing on how to handle different languages. (I18N).

Download base project

This tutorial uses the flow-helloworld-maven-meecrowave as a base. Read more about it here

You are able to find the latest version of the source code for this tutorial @github

It is recommended to read the tutorial about the I18NProvider first. This tutorial will use the I18NProvider later for the translations.

Setting the page title

In this solution, the title is set inside a constructor of a view.

  public class View001() {
    UI current = UI.getCurrent();
    Locale locale = current.getLocale();
    current
        .getPage()
        .setTitle(Messages.get("global.app.name" , locale)
                  + " | "
                  + Messages.get("view001.title" , locale));
  }

Here are some points I would like to discuss. The first is based on the assumption that your application is not only based on one view. Are you sure you want to write this piece of code a few times? And how often your colleagues in your team will forget this? The next is, that there is an implicit definition of a format for the page title. xx + " | " + yy

We have to extract the repeating part. This could lead to version 02…​

Solution 2A - extracting the formatter

We are extracting the formatting part first. For this a class with the name TitleFormatter is created. Inside you can define how the page title will be formatted.

public class TitleFormatter {
 public String format(String key, Locale locale){
 return getTranslation("global.app.name" , locale)
 + " | "
 + getTranslation(key , locale);
 }
}

With this class, we have a central place for the definition of how the title should look like. The next step is to remove the invocation method out of the constructor. Vaadin provides an interface called HasDynamicTitle. Implementing this, you can overwrite the method getPageTitle(). But again, you would implement this in every view…

@Route(View002.VIEW_002)
public class View002 extends Composite<Div> implements HasDynamicTitle {
  public static final String VIEW_002 = "view002";

  @Override
  public String getPageTitle() {
    UI current = UI.getCurrent();
    Locale locale = current.getLocale();
    return new TitleFormatter().format("view.title", locale);
  }
}

One solution could be based on inheritance, packing this stuff into a parent class. But how to get the actual key to resolve without implementing something in every child class?

Solution 2B - using the @PageTitle annotation

Vaadin will give you one other way to define the page title. The other solution is based on an annotation called PageTitle

@PageTitle("My PageTitle")
@Route(View002.VIEW_002)
public class View002 extends Composite<Div> {
  public static final String VIEW_002 = "view002";

}

It can only be used the Annotation or the interface HasDynamicTitle. Both together in one class will not work.

So, make sure that there is nothing in your inheritance.

The challenge here is based on the fact that the annotation only consumes static Strings. I18N is not possible with this solution.

Solution 3 - my favourite solution ;-)

After playing around with these solutions, I developed a solution that could handle

  • message bundles

  • is not inside inheritance

  • is based on annotations

  • is easy to extend

  • can change the language during runtime

The developer / user view

Mostly it is a good approach to develop a solution for a developer from the perspective of a developer. Here it means, what should a developer see if he/she have to use your solution.

The developer will see this annotation. Three things can be defined here.

  • The message key that will be used to resolve the message based on the actual Locale

  • A default value the will be used, if no corresponding resource key was found and no fallback language is provided

  • Definition of the message formatter, default formatter will only return the translated key.

@Retention(RetentionPolicy.RUNTIME)
public @interface I18NPageTitle {
  String messageKey() default "";
  String defaultValue() default "";
  Class< ? extends TitleFormatter> formatter() default DefaultTitleFormatter.class;
}

The default usage should look like the following one.

@Route(View003.VIEW_003)
@I18NPageTitle(messageKey = "view.title")
public class View003 extends Composite<Div> implements HasLogger {
  public static final String VIEW_003 = "view003";
}

Now we need a way to resolve the final message and the right point in time to set the title. Here we could use the following interfaces.

  • VaadinServiceInitListener,

  • UIInitListener,

  • BeforeEnterListener

With these interfaces, we are able to hook into the life cycle of a view. At this time slots we have all the information we need. The annotation to get the message key and the locale of the current request is available.

The class that is implementing all these interfaces is called I18NPageTitleEngine

public class I18NPageTitleEngine
       implements VaadinServiceInitListener,
                  UIInitListener, BeforeEnterListener, HasLogger {

  public static final String ERROR_MSG_NO_LOCALE = "no locale provided and i18nProvider #getProvidedLocales()# list is empty !! ";
  public static final String ERROR_MSG_NO_ANNOTATION = "no annotation found at class ";

  @Override
  public void beforeEnter(BeforeEnterEvent event) {
    Class<?> navigationTarget = event.getNavigationTarget();
    I18NPageTitle annotation = navigationTarget.getAnnotation(I18NPageTitle.class);
       if(annotation == null) {
      logger().info(ERROR_MSG_NO_ANNOTATION + navigationTarget.getName());
    } else {
      final String messageKey = (annotation.messageKey().isEmpty())
                          ? annotation.defaultValue()
                          : annotation.messageKey();

      final I18NProvider i18NProvider = VaadinService
          .getCurrent()
          .getInstantiator()
          .getI18NProvider();
      final Locale locale = event.getUI().getLocale();
      final List<Locale> providedLocales = i18NProvider.getProvidedLocales();

      Locale providedLocale = null;

      if(locale == null && providedLocales.isEmpty()){
        logger().info(ERROR_MSG_NO_LOCALE + i18NProvider.getClass().getName());
      } else if(locale == null){
        providedLocale = providedLocales.get(0);
      } else if(providedLocales.contains(locale)) {
        providedLocale = locale;
      } else {
        providedLocale = providedLocales.get(0);
      }

      final Class<? extends TitleFormatter> formatterCls = annotation.formatter();

      try {
        final TitleFormatter formatter = formatterCls.getDeclaredConstructor().newInstance();
        formatter.apply(i18NProvider , providedLocale , messageKey)
        .ifPresentOrElse(txt -> UI.getCurrent()
                                    .getPage()

                                .setTitle(txt),
                         failed -> logger().info(failed)
        );

      } catch (InstantiationException e) {
        e.printStackTrace();
      } catch (IllegalAccessException e) {
        e.printStackTrace();
      } catch (InvocationTargetException e) {
        e.printStackTrace();
      } catch (NoSuchMethodException e) {
        e.printStackTrace();
      }
    }
  }

  @Override
  public void uiInit(UIInitEvent event) {
    final UI ui = event.getUI();
    ui.addBeforeEnterListener(this);
  }

  @Override
  public void serviceInit(ServiceInitEvent event) {
    event
        .getSource()
        .addUIInitListener(this);
  }
}

The method with the name beforeEnter is the important part. Here you can see how the key is resolved. But there is one new thing – let’s have a look at the following lines.

              final I18NProvider i18NProvider = VaadinService
                  .getCurrent()
                  .getInstantiator()
                  .getI18NProvider();

This few lines are introducing a new thing that is available in Flow. The interface I18NProvider is used to implement a mechanism for the internationalization of Vaadin applications.

To read more about it go to our I18NProvider Tutorial here

Last step is the activation of our I18NPageTitleEngine This is done inside the file with the name com.vaadin.flow.server.VaadinServiceInitListener you have to create inside the folder META-INF/services The only line we have to add is the fully qualified name of our class.

com.vaadin.tutorial.flow.i18n.pagetitle.I18NPageTitleEngine

If you have questions or something to discuss, add a comment below.

Vaadin is an open-source framework offering the fastest way to build web apps on Java backends
GET STARTED

Comments (4)

Sven Ruppert
2 years ago Jun 21, 2019 8:06am
Lukas Schär
2 years ago Jun 21, 2019 8:19am