-
Notifications
You must be signed in to change notification settings - Fork 17
Adding custom CSS
There are several ways to add custom CSS to one's WComponents application. Under most circumstances this should be unnecessary as the theme should provide a common look and feel for the application suite.
Usually the theme should provide all the necessary styling, the following are some reasons why one may need to add custom CSS for a view:
- responsive design as this is not really possible in an abstract framework as it requires knowledge of the application's layout to determine where items can be compressed, re-ordered or removed;
- local changes to individual instances of a control;
- user choice.
The CSS applied in a WComponents application is attached in the HTML output stream in the following order:
- base theme CSS is added to the head element; then
- platform and browser specific theme CSS is added to the head element; then
- CSS links created using ui:css /
WApplication.addCssUrl
orWApplication.addCssFile
are added to the head element; then - HTML link elements (with rel='stylesheet') added to any container component in the application in their order in the source tree are output into the head element; then
- HTML style elements added to any container component in the application in their order in the source tree are output in-situ (at the moment) wherever they are in the input stream.
Note that #5 above is under eternal review as there are competing interests of XSLT performance and a preference to have all style elements which do not have a scoped attribute set placed into the head element where they belong. If we eventually hoist unscoped style elements to the head they will be placed after the application specific link elements so will remain last in order.
The most efficient way to add CSS is usually using a cacheable stylesheet. It is not difficult to add one of these, what it should contain is another matter and is dealt with below.
New in WComponents 1.0.3
WApplication
has these methods which may be used to add a link element to the page head element:
-
addCssUrl(String url)
; and -
addCssFile(String fileName)
.
Each of these methods has the same net effect: a HTML link element will be added to the head linking to the url or file resource.
// Get the WApplication from the current object'
WApplication app = WebUtilities.getAncestorOfClass(WApplication.class, this);
// Then use this WApplication to add CSS from a resource.
app.addCssFile("/path/to/resource/my_css_file.css");
// Adding from a URL is similar. Use '//'' to avoid defining the protocol.
app.addCssUrl("//example.com/path/to/static_css_file.css");
This technique is being phased out in favour of that outlined above but is available in WComponents 1.0.2 and before. The methods for setting and getting headlines shown in the example are all deprecated.
A CSS stylesheet can be added to an application using com.github.bordertech.wcomponents.Headers.addUniqueHeadLine(String type, String aLine)
or com.github.bordertech.wcomponents.Headers.addUniqueHeadLine(String aLine)
.
The advantage of this is that the CSS will be downloaded by the browser and potentially cached (dependent on user settings).
We are in the process of adding methods to WApplication
to make the process of adding a CSS file link less obscure. In the meantime one possible method of doing this could be by using WContent and an internal resource :
private final WContent css = new WContent();
// ...
// Add a cache key to css so that it can be cached. This cache key
// can then be changed if there is a need to update the CSS.
css.setCacheKey("my.custom.css" + version);
// The resource must be added to the output tree event though it is
// not included in the XML output.
add(css);
// ...
// At some point the css resource must be set up. This could be the
// first time the object to which css was 'added' is painted in
// preparePaintComponent
@Override
protected void preparePaintComponent(final Request request) {
super.preparePaintComponent(request);
// ...
if (!isInitialised()) {
// ...
css.setContentAccess(new InternalResource(
"/path/to/resource/my_custom_stylesheet.css",
"my_custom_stylesheet.css"));
setInitialised(true);
}
// ...
// Finally use addUniqueHeadLine To attach the css
UIContext uic = UIContextHolder.getCurrent();
uic.getHeaders().addUniqueHeadLine(
"<link type='text/css' rel='stylesheet'" +
"href='" + WebUtilities.encode(css.getUrl()) + "'/>");
}
WARNING this could be considered a dirty hack and your particular theme may not support it, but it will work in the default theme.
If the stylesheet is at a fixed URL it may be able to add it to the application by building a HTML link element. To do this one could use WText as per the example below:
// NOTE that the link element must be XML compliant so must be self closing.
private static final String CSS_LINK_CONTENT =
"<link type='text/css' rel='stylesheet'" +
" href='//example.com/my_custom_stylesheet.css' />";
private final WText cssLink = new WText(CSS_LINK_CONTENT);
// ...
// Somewhere we need to turn off text encoding and add the WText.
cssLink.setEncodeText(false);
add(cssLink);
This will write a HTML link element to the output stream. The default theme XSLT will hoist this link into the HTML head element.
The main downside of this method (and why it is a dirty hack is that one has to be aware of what the consequences will be of adding a WText with no UI artefact to any given container. If the WText is added to a WPanel layout it will reserve a cell for the WText. This may result in unexpected alignment or spacing. If, for example, the WText is added to a ColumnLayout then it will result in an empty cell in the row and column into which the WText is added. There are simple techniques for avoiding this.
As at January 2016 WComponents supports Velocity templates through a package protected method exposed in WContainer (and therefore WPanel). We are working on a replacement for this which should make adding custom templates easier and more efficient. If a WPanel has a Velocity template then that template is used to render the panel, not its XML renderer. All components added to the WPanel have to be tagged for inclusion in the velocity template.
It would probably be overkill to create a WPanel with a velocity template just to add a CSS link but it is feasible.
Sometimes a little bit of CSS has to be added to particular renderings or user contexts. We are also working on ways to make this easier but the current method is to use WText or a Velocity template.
An example of using WText is shown below. A much more convoluted example is in the wcomponents-examples com.github.bordertech.wcomponents.examples.theme.WRowExample.addAppLevelCSSExample()
.
// ...
// in amongst the class members
// Something to hold the custom CSS
final private cssText = new WText();
// Something to style - this can be any component with a UI artefact
// but some are easier than others. It can be initialised later.
private WStyledText inverseText;
// ASSUME we have a specific CSS requirement for ease of example
private static final String CSS_BLOCK
= "{background-color:white;color:black;padding:0.25em;}";
// ...
// Make sure we turn of encoding - e.g. in the constructor.
cssText.cssText.setEncodeText(false);
// and if we do not add we do not get ...
add(cssText);
//...
// later, in preparePaintComponent
// this could be in an override of preparePaintComponent for inverseText
// if necessary.
@Override
protected void preparePaintComponent(final Request request) {
super.preparePaintComponent(request);
if (!isInitialised()) { // do this only once.
// A better Java developer would use StringBuilder or StringBuffer:
// I am a lazy JS hack!
cssText.setText("<style type='text/css' scoped='scoped'>#"
+ inverseText.getId()
+ CSS_BLOCK + "</style>");
setInitialised(true); // do this only once.
}
}
If the component being styled has has its htmlClass property set then this can be used as a selector instead of an ID. This has a slight advantage over using an ID in that the htmlClass is not changed by inclusion in a naming context whereas a components ID is. The most significant disadvantage of using getHtmlClass()
as the only CSS selector is that it may not be specific enough to change theme CSS.
The simplest selector is an ID selector as it will always be more specific than any selector in the WComponents theme. Even here it may sometimes be necessary to set some rules as !important
in order to override the inline styles we sometimes have to write in order to support structural layout abstractions such as column widths, hgap
and vgap
.
The main problem with ID selectors is that a WComponent's ID is not necessarily fixed. Even if a static ID is set using setId(String id)
it cannot be guaranteed that the CSS selector #SOME_ID
will work as IDs are modified if the component is nested within a naming context. There are two ways around this issue. One, which is guaranteed to always work but which is less efficient is to write the CSS inline and use getId()
during preparePaintComponent
to build the selector. This is illustrated above in the section on inline CSS.
An alternative which allows for cacheable style sheets depends on the full structure of all uses of a component being known. This is extremely fragile and is not recommended.
If this structure is known and the component is either never in a naming context (other than the default WApplication) or is always in a known nesting of naming context(s) where the context ID prefixes are known then the ID will not change.
Given a WButton, for example, where the button has been given the ID "fancySubmitButton" we have two possible fixed ID selectors:
- if the button is never in a naming context then the selector will be
#fancySubmitButton
; - if the button is always in a known naming content "someContext" and a nested naming context "someSubContext" then the selector will be
#someContext-someSubContext-fancySubmitButton
.
I told you it was fragile! Maybe go look at WComponents ID property it might help.
WComponents default theme uses a lot of attribute selectors. Many of these selectors (indeed, most of them) are tied to WAI-ARIA roles, states and properties. These WAI-ARIA based attribute selectors are as safe as any for use in custom styles.
Given a WTab, for example, it is safer to create a CSS selector using the attribute selector [role='tab']
than to rely on the XML element local name tab
to build a class selector .tab
. Wherever possible WComponents will use WAI-ARIA roles, states and properties as the basis for JavaScript functionality and default styling so if something loses its role of tab
it will stop behaving like a tab as well as no longer looking like a tab.
Class selectors should only be used to add additional styles to a component to which a class attribute value has been added using setHtmlClass(String className)
Class selectors are much easier to deal with than ID selectors but are much less specific. One may have to use some cunning to get a selector specific enough to override theme determined styles. Knowledge of the CSS selectors used in the default WComponents theme may (should) help but these are not strictly enforceable in any application theme as the rendering is done in XSLT which is itself part of the theme and may be overridden.
There are a few groups of inbuilt classes; these are discussed at some length in Theme class name conventions. It must be noted that there is no guarantee your theme will support any of these, even support for the first is recommended and never enforced.
WComponents which extend AbstractWComponent
have a method setHtmlClass(String htmlClass)
to add value(s) to the class attribute of the root output element. More information may be found here.
It is possible to add a bunch of "words" to the class attribute using setHtmlClass
giving one the option to create quite specific selectors but if one needs to go to these lengths it may be worthwhile revisiting one's theme.