Adding Custom HTML to your TAdapterPageProducers

By: Nick Hodges

Abstract: This article provides a component framework and some sample components for adding custom HTML as child components to TAdapterPageProducers

This code is part of my talk at Borcon 2002 entitled "Building WebSnap Components". Be sure to attend for more information on enhancing WebSnap.

The Obligatory Introduction

So far, I've written a series of articles on building WebSnap applications, paying most of the attention to how to get the existing WebSnap components to work and do the things that you want to for your website. However, now I think it is time to change the pace a little bit and work on some custom WebSnap components. It is in building these components that you can really bring out the power of WebSnap in your applications.

The Much Maligned TAdapterPageProducer

One of the more powerful components in WebSnap is the TAdapterPageProducer. It can be used to create complex web pages using TAdapter components and their fields and actions. If you have been following along with my earlier articles, you are familiar with the component and what it can do. Some folks in the Borland newsgroups have maligned – unfairly, I think – the TAdapterPageProducer (You know who you are. ;-). It has its weaknesses and limitations, yes, but I don't think it should be so easily dismissed. Granted, it can't always do what a web developer might need, but it can be quite powerful for producing HTML and Javascript that can then be imported into a regular TPageProducer and managed manually. I like the TAdapterPageProducer, though, and am sticking with it, as I am confident that Borland's R&D lab has great things cooked up for it in the future.

In any event, out of the box, there is a limited amount of functionality that it provides, but that functionality can easily be quickly enhanced by adding custom components to it that allow you to add any type of HTML that you want almost anywhere within it. This capability alone will add power to the TAdapterPageProducer right away. In addition, we'll design these components in such a way that makes it easy to build any type of HTML based components to add to a TAdapterPageProducer. So away we go!

The Art of the Abstract

When ever you want to build a class that will be malleable and able to have lots of descendants with similar functionality but slightly different implementation, you should always consider creating an abstract class. That's what I've done here. We are going to want to create a set of components that do one thing – produce HTML. Each component you'll want will likely produce slightly different HTML, but they'll all do that one thing. So maybe we can create an abstract class where all you'd need to do to descend from it would be to call a single method called, say, GetHTML. What a great idea, eh? Well, let's do it:

  TnxBaseWebSnapComponent = class(TWebContainedComponent, IWebContent)
  protected
    { IWebContent }
    function Content(Options: TWebContentOptions; ParentLayout: TLayout): string;
    function GetHTML: string; virtual; abstract;
  end;

Now, TnxBaseWebSnapComponent is a cool class. First, notice that it descends from TWebContainedComponent, which is the parent class for all components that fit into a TAdapterPageProducer. It basically knows how to be owned, parented and managed by a TAdapterPageProducer. In and of itself it doesn't do anything, but provides the framework for being a TAdapterPageProducer component.

Next, you should have noticed that the class declares and implements the IWebContent interface. That interface is declared like this:

  IWebContent = interface
  ['{1B3E1CD1-DF59-11D2-AA45-00A024C11562}']
    function Content(Options: TWebContentOptions;
      ParentLayout: TLayout): string;
  end;

This is obviously a simple interface that when implemented, merely asks for the content of the component, normally as HTML. Hey, that's exactly what we need! What a coincidence, don't you think?

Thus, next you'll notice that our class does indeed implement the IWebContent interface. The implementation of the single method goes like this:

{ TnxBaseWebSnapComponent }
function TnxBaseWebSnapComponent.Content(Options: TWebContentOptions;
  ParentLayout: TLayout): string;
var
  Intf: ILayoutWebContent;
begin
  if Supports(ParentLayout, ILayoutWebContent, Intf) then
  begin
    Result := Intf.LayoutField(GetHTML, nil)
  end else
  begin
    Result := GetHTML;
  end;
end;

This code basically just retrieves the HTML that your component will generate via the call to the GetHTML call. It does it in two different ways. If the ParentLayout parameter, i.e. the component into which this HTML will be placed, implements the ILayoutWebContent interface, then the ParentLayout will be responsible for fitting the HTML into the TAdapterPageProducer. And example of this might be if you were trying to put HTML into a special type of grid column, then the grid column would be responsible for properly formatting the HTML you are returning. Otherwise, just the HTML itself is returned.

Of course, the next thing that you'll notice in the TnxBaseWebSnapComponent is the abstract virtual method, GetHTML. Abstract, virtual methods scream to be overridden. Since this is an abstract class, and the GetHTML call is made in the Content method, the wonders of polymorphism will allow your descendant components to worry about nothing other than implementing the GetHTML method. And that's what we'll do right now.

Get Your HTML While its Still Hot!

Okay, probably the most basic thing you can do here is to simply add HTML – any HTML – via this component. So we'll declare the following component, TnxHTMLCode, which will do nothing more than insert HTML into a TAdapterPageProducer.

TnxHTMLCode = class(TnxBaseWebSnapComponent)
  private
    FHTML: TStrings;
    procedure SetHTML(const Value: TStrings);
  protected
    function GetHTML: string; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property HTML: TStrings read FHTML write SetHTML;
  end;

This class implements the abstract method GetHTML and declares (and manages) a single property of type TStrings named HTML. The GetHTML method is so simple that I almost don't have to show it, but I will:

function TnxHTMLCode.GetHTML: string;
begin
  Result := FHTML.Text;
end;

I don't need to explain this code, do I? <g> Thus, when this component is added to the TAdapterPageProducer, and the HTML property filled in, whatever is in that HTML property will be placed right into the TAdapterPageProducer at the spot where the TnxHTMLCode component resides. How could that be any simpler? For your further edification, however, it is not that simple behind the scenes. What happens is that the TAdapterPageProducer component will cycle through all the components it holds, grabbing the IWebContent interface for each, and then calling the Content method on that interface. Thus, it is clear that any component that wants to be a TAdapterPageProducer component needs to implement the IWebContent interface, right? See how that works? Cool, huh? This is a great component because it illustrates the use of interfaces, abstract classes, and polymorphism.

Let's Get a Bit More Specific

Okay, now you see how a simple HTML component works, there are other more specific uses that you might want to implement. For instance, you might want to insert the contents of an existing HTML into your web page. That's easily done, as you might guess. Here's how I did it:

TnxWebFile = class(TnxBaseWebSnapComponent)
  private
    FHTMLFile: TFilename;
    FHTMLDoc: TStrings;
    FFileInDFM: Boolean;
    procedure SetHTMLDoc(const Value: TStrings);
    procedure SetHTMLFile(const Value: TFilename);
    procedure SetFileInDFM(const Value: Boolean);
  protected
    function GetHTML: string; override;
    function LoadFile: string;
  public
    constructor Create(AComponent: TComponent); override;
    destructor Destroy; override;
  published
    property FileInDFM: Boolean read FFileInDFM write SetFileInDFM;
    property HTMLDoc: TStrings read FHTMLDoc write SetHTMLDoc;
    property HTMLFile: TFilename read FHTMLFile write SetHTMLFile;
  end;

This class, again, overrides the GetHTML method to grab the HTML from the file, and three properties that manage the HTML file. The first, FileInDFM, determines whether the contents of the file are copied into the HTMLFile property, or whether the file remains outside of the binary application, where it can be changed manually and where those changes will be immediately shown in the next page request. So, that pretty much explains the other two properties, HTMLDoc and HTMLFile.

The important implementations are done like this:

function TnxWebFile.GetHTML: string;
begin
  if FileInDFM then
  begin
    Result := HTMLDoc.Text;
  end else
  begin
   Result := LoadFile;
  end;
end;

function TnxWebFile.LoadFile: string;
var
  FS: TFileStream;
  SS: TStringStream;
begin
  if (HTMLFile <> '') and FileExists(HTMLFile) then
  begin
    FS := TFileStream.Create(HTMLFile, fmOpenRead);
    SS := TStringStream.Create('');
    try
      SS.CopyFrom(FS, FS.Size);
      Result := SS.DataString;
    finally
      FS.Free;
      SS.Free;
    end;
  end else
  begin
    Result := '';  // if the file doesn't exist, then return nothing
  end;
end;

procedure TnxWebFile.SetFileInDFM(const Value: Boolean);
begin
  // Note: if this is True, then the contents of the file are added to the HTMLDoc property, and thus
  // written to the DFM file.  If that is the case, and you make changes to the file, they _won't_ be reflected
  // in the component.  If this is False, then the component will hunt up the file each time that it is asked for
  // and sent as Content.  In that case, you can change the file, and those changes will reflect in the next view
  if FFileInDFM <> Value then
  begin
    FFileInDFM := Value;
    HTMLDoc.Clear;
    if FFileInDFM then
    begin
      HTMLDoc.Add(LoadFile);
    end;
  end;
end;

The GetHTML method returns either the contents of the HTMLDoc.Text property, or the LoadFile method, depending on the value of the FileInDFM property. LoadFile does what you'd expect it to given its name, and returns a string with the contents of the file named in the HTMLFile property. Finally, the SetFileInDFM method manages the content in the HTMLDoc property, clearing or setting the values in it depending on the SetFileInDFM also.

Now, when you add this component and set the HTMLFile property, the contents of that file will be added to the TAdapterPageProducer. You could use this to manage, say, an article like this one as an external HTML file, but easily integrate it into a page template for your site. As a matter of fact, that is exactly what I do with the articles on my site.

One More Component

TnxHTMLCode shows how to add any HTML to your web page. There may be times, however, when you want to add specific types of HTML, and our component architecture can handle that as well. For example, there may be times when you want to add a Javascript code section to your page. Maybe you have a number of different standard code snippets that you like to add to pages as needed.

We can easily create a component that will add a Javascript block. In fact, it will be designed almost the same as the TnxHTMLCode component, but instead of an HTML property, it will have a Javascript property of type TStrings, and its GetHTML method will properly format the Javascript. The class declaration will look like this:

TnxJavascriptEntry = class(TnxBaseWebSnapComponent)
  private
    FJavascript: TStrings;
    procedure SetJavaScript(const Value: TStrings);
  protected
    function GetHTML: string; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property Javascript: TStrings read FJavascript write SetJavaScript;
  end;

The GetHTML function is implemented like so:


function TnxJavascriptEntry.GetHTML: string;
const
  CRLF = #13#10;
  JSTemplate = CRLF + '<SCRIPT LANGUAGE="JavaScript">' + CRLF +
                '%s' + CRLF +
                '</SCRIPT>'+ CRLF;
begin
  Result := Format(JSTemplate, [FJavascript.Text]);
end;

Again, it's easy to create these components because all you have to do is manage the properties you need to customize your HTML, and then implement the GetHTML method to put it all together. You could easily create components to add Delphi code entries, links, graphics, or other elements to your HTML code. The simple abstract class TnxBaseWebSnapComponent does all the plumbing to keep the TAdapterPageProducer happy, and all you need to do is worry about creating the appropriate HTML.

Registering All of This

Of course, like any component, you need to register all of these classes with Delphi. However, unlike registering regular components on the palette, registering WebSnap components is a bit more complicated, and in the case of TAdapterPageProducer components, it is a bit more complicated still. The declaration for this unit is as follows:

unit nxHTMLCompsReg;

interface

procedure Register;

implementation

uses Classes, WebComp, MidItems, WebForm, nxHTMLComps;

type
  THelper = class(TWebComponentsEditorHelper)
  protected
     function ImplCanAddClassHelper(AEditor: TComponent; AParent: TComponent; AClass: TClass): Boolean; override;
  end;

var
 nxHelper: THelper;

{ THelper }
function THelper.ImplCanAddClassHelper(AEditor: TComponent; AParent: TComponent; AClass: TClass): Boolean;
begin
  // Return True to indicate that AParent is a valid parent of the components being registered.
  Result := AEditor.InheritsFrom(TCustomLayoutGroup) or
    AEditor.InheritsFrom(TCustomAdapterForm);
end;

procedure Register;
begin
  RegisterWebComponents([TnxWebFile, TnxHTMLCode, TnxJavascriptEntry], nxHelper);
end;

initialization
  // Helper is used at design-time to indicate which components can parent the THTMLCode components
  nxHelper := THelper.Create;

finalization
  UnregisterWebComponents([TnxHTMLCode, TnxWebFile, TnxJavascriptEntry]);
  nxHelper.Free;
end.

The code is pretty straightforward, but there are some things to note:

  • The unit has a Register procedure just like any design time registration unit.

  • The three classed in question are registered with a call to RegisterWebComponents in the Register procedure, but then must be unregistered with a call to UnregisterWebComponents in the finalization section of the unit

  • The call to RegisterWebComponents takes as its second parameter the instance of a class that descends from THelper. In this case, we declared TnxHelper. You need to implement just the ImplCanAddClassHelper method, and return true if the classes being registered can be added to the class type passed in to the function. In this case, the components can be added to any TCustomLayoutGroup or TCustomAdapterForm descendants. This class is the one that tells the IDE which classes can become sub-components of other classes – i.e., it tells the IDE which classes to list when you select a component in the Web Surface Designer and press the Add Component button

  • The TnxHelper class instance is created in the initialization section of the unit and destroyed after the components are unregistered in the finalization section.

The Obligatory Conclusion

This is a pretty cool component, because the possibilities are endless. You can easily create components that build all sorts of HTML constructs based on properties the user can set. Simply use those properties to build HTML based on a template and you are off to the races. As an aside, a number of files in the <delphi>sourceinternet directory have routines for managing and building HTML, so take a look there and see what you can ferret out.

As always, the code for the components can be found on Code Central.

Nick Hodges is the Chief Technology Officer at Lemanix Corporation, a consulting shop specializing in Delphi Development. He likes to read Stephanie Plum novels and keep track of the Minnesota Timberwolves. But mostly he like to hang with his family and enjoy their new home in St. Paul, MN.

Server Response from: ETNASC03