Write helpers for wrapping HTML elements

244 Views Asked by At

I am currently writing my own Component base class with a whole bunch of helper methods for using the Twitter Bootstrap CSS framework (and avoiding all the boilerplate code around it). They can be used very much like the existing form helpers (like html div ...).

E.g., the one for horizontal text fields (in a horizontal form) looks like this:

horizontalTextField: aLabel
| field |

field := WATextInputTag new
    class: 'input-xlarge';
    yourself.

self html div
    class: 'control-group';
    with: [
        self html label
            class: 'control-label';
            with: aLabel.
        self html div
            class: 'controls';
            with: [self html brush: field].
    ].

^ field.

Which I aim to be using like this when rendering a component:

(self horizontalTextField: 'Titel')
    on: #title of: self article;
    id: 'title'.

So, the aim is to wrap the actual text field in a few divs, but still be able to make changes to this wrapped element outside of the helper function (using the normal tag helper accessors), as shown above.

However, this does not work as the with: method causes the wrapping divs to be serialized (aka rendered) before I return the element, which I then cannot edit anymore.

POSSIBLE SOLUTIONS:

  1. Actually extend the WACanvasTag subclasses used for rendering the fields and create instances of those new subclasses in my custom helpers in my component class. I would simply overwrite their rendering code for my pleasure. This would probably be the most object-oriented way to do things, but quite arduous, especially as I'd have to repeat quite a lot of code for inserting my own HTML before and after the element for every single one of these subclasses (as they already have to inherit from the Tag class which I'm trying to wrap).
  2. When calling the helpers, do something like self horizontalTextField: 'Titel' with: [:field | id: 'title']. The block would then be applied before rendering the actual text field in the helper method. This would be quite flexible, but not very pretty (syntax-wise).
  3. Hardcode the wrapping HTML (related to this question) in my helper functions. Like this: self html html: '<label class="control-label"> etc. Quite the hack, in a way, and not very object-oriented.

Comments? Ideas? Better suggestions?

2

There are 2 best solutions below

0
On

It looks like you want to use it like a Tag Brush, so I'd say you definitely want to go with the extending WACanvasTag approach - but only when you actually want to create a compound "tag" like in the horizontalTextField situation.

For other Bootstrap concepts - like row, container, etc, your best bet is to simply add extension methods to WAHtmlCanvas.

I've done this myself - here's my implementation of a Bootstrap NavBar tag:

WABrush subclass: #BSNavBar
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Bootstrap'

BSNavBar>>with: aBlock
    super with: [
        canvas div class: 'navbar'; with: [
            canvas div class: 'navbar-inner'; with: [
                canvas container: aBlock]]].
    closed := true

And here's how I've implemented "row" -

WAHtmlCanvas>>row
    ^self
        div class: 'row'

WAHtmlCanvas>>row: aBlock
    self row with: aBlock

In addition I added extension methods to WATagBrush to support span, offset and pull-right, as well as fluid containers:

WATagBrush>>offset: anInteger
    self class: 'offset', anInteger asString

WATagBrush>>beFluid
    self attributeAt: #class 
        put: (((self attributeAt: #class ifAbsent: [^self]) 
            copyReplaceTokens: 'container' with: 'container-fluid')
            copyReplaceTokens: 'row' with: 'row-fluid')

And finally, here's an example render method that uses some of the above:

renderContentOn: html
    html navBar: [
        html div pullRight; with: [ 
            self session isLoggedIn 
                ifTrue: [self renderUserMenuOn: html] 
                ifFalse: [self renderLoginBoxOn: html]]]
3
On

As you noticed, the above code does not work because the HTML Canvas emits the HTML right away. Brushes should never be stored anywhere, they are only valid a very short time. The same goes for the HTML canvas, it is very uncommon and a possible cause of bugs to store it somewhere.

The typical way to do this in Seaside is to create a helper method:

renderHorizontalLabel: aLabelRenderer andField: aFieldRenderer on: html
   html div
       class: 'control-group';
       with: [
           html label
               class: 'control-label';
               with: aLabelRenderable.
           html div
               class: 'controls';
               with: aFieldRenderer ]

The nice thing about the above code is that both aLabelRenderer and aFieldRenderer can be any object implementing #renderOn: (String, Number, Block, WAPresenter, WAComponent, ...).

renderContentOn: html
   self
       renderHorizontalLabel: 'Comment'
       andField: [
           html textField
               value: comment;
               callback: [ :value | comment := value ] ]
       on: html.
   self
       renderHorizontalLabel: 'Acknowledge'
       andField: [ self renderCheckbox: false on: html ]
       on: html.
   ...

To generate the actual field you could create other methods that you then call from the block you pass in as aFieldRenderer. This gives you the flexibility to arbitrarily compose the different parts. Have a look at the examples in Seaside, there are many users of this pattern.