Skip to content
This repository was archived by the owner on Apr 13, 2023. It is now read-only.
This repository was archived by the owner on Apr 13, 2023. It is now read-only.

change pattern for attributes in ceylon.html #697

@ghost

Description

Somewhere “fluent” APIs win over Ceylon’s named argument syntax is when there is inheritance. With a class that specifies a bunch of arguments (with potentially a lot being defaulted), every subclass that wants to allow these arguments to be specified during construction must explicitly declare the same arguments and specify the same default values.

This can become a burden and is especially bothersome if the class is open for other modules to inherit from. A parameter’s default value might be an implementation detail (and might change from version to version). Requiring that it be specified in a subclass out of its module is unacceptable.

Additionally, it makes it hard to extend the API: if someone wants to add a new parameter at the end of the class, one must do so for every single subclass. If there is a trailing iterable parameter, it becomes impossible to do so without ruining backward compatibility.

With the builder pattern, fluent APIs allow similar (yet slightly different) instances to be produced very easily.

Together with (and even without) the builder pattern, fluent APIs win over the current named argument pattern in almost every aspect. The only case I can see in which fluent APIs lose is when a subclass doesn’t want to allow a specific set of parameters of the superclass to be specified.

Now, differently from what you may have been thinking, I am not going to propose to use a fluent API on ceylon.html, I am actually going to propose an entirely new pattern. I call it “the attributes pattern”.

The attributes pattern is pretty straightforward. You just write an interface to represent the parameters (here called Attributes). For ceylon.html, it ought to look something like the following.

shared interface Attributes
{
	shared Attribute<String> id = null;
	shared Attribute<[String+]> classes = null;
	shared Attribute<String> accessKey = null;
	shared Attribute<Boolean> contentEditable = null;
	shared Attribute<String> contextMenu = null;
	shared Attribute<Direction> dir = null;
	shared Attribute<Boolean> draggable = null;
	shared Attribute<DropZone> dropZone = null;
	shared Attribute<Boolean> hidden = null;
	shared Attribute<String> lang = null;
	shared Attribute<Boolean> spellcheck = null;
	shared Attribute<String> style = null;
	shared Attribute<Integer> tabIndex = null;
	shared Attribute<String> title = null;
	shared Attribute<Boolean> translate = null;
}

Then, the Element class can look like:

shared abstract class Element(shared String tagName, Arttributes attributes = object satisfies Attributes {}, {Content<Node>*} children = [])
	extends Node(...)
{
	shared String? id = getAttribute(attributes.id);
	shared [String+]? classes = getAttribute(attributes.classes);
	shared String? accessKey = getAttribute(attributes.accessKey);
	shared Boolean? contentEditable = getAttribute(attributes.contentEditable);
	shared String? contextMenu = getAttribute(attributes.contextMenu);
	shared Direction? dir = getAttribute(attributes.dir);
	shared Boolean? draggable = getAttribute(attributes.draggable);
	shared DropZone? dropZone = getAttribute(attributes.dropZone);
	shared Boolean? hidden = getAttribute(attributes.hidden);
	shared String? lang = getAttribute(attributes.lang);
	shared Boolean? spellcheck = getAttribute(attributes.spellcheck);
	shared String? style = getAttribute(attributes.style);
	shared Integer? tabIndex = getAttribute(attributes.tabIndex);
	shared String? title = getAttribute(attributes.title);
	shared Boolean? translate = getAttribute(attributes.translate);
}

Subclasses can be much shorter. A subclass that doesn’t introduce any new attributes can merely accept the same Attributes interface and pass it up to the superclass for it to use.

shared class Div(Attributes attributes = object satisfies Attributes {}, {Content<FlowCategory>*} children = [])
	extends Element("div", attributes, children)
{
}

However, subclasses that do need to declare more attributes can define a subtype of Attributes and specify them there.

shared interface FormAttributes
	satisfies Attributes
{
	Attribute<String> acceptCharset = null;
	Attribute<String> action = null;
	Attribute<Boolean> autocomplete = null;
	Attribute<String>|Attribute<FormEnctype> enctype = null;
	Attribute<String>|Attribute<FormMethod> method = null;
	Attribute<String> name = null;
	Attribute<Boolean> novalidate = null;
	Attribute<String>|Attribute<FormTarget> target = null;
}

shared class Form(FormAttributes attributes = object satisfies FormAttributes {}, {Content<FlowCategory>*} children = [])
	extends Element("form", attributes, children)
{
	String acceptCharset = getAttribute(attributes.acceptCharset);
	String action = getAttribute(attributes.action);
	Boolean autocomplete = getAttribute(attributes.autocomplete);
	String|FormEnctype enctype = getAttribute(attributes.enctype);
	String|FormMethod method = getAttribute(attributes.method);
	String name = getAttribute(attributes.name);
	Boolean novalidate = getAttribute(attributes.novalidate);
	String|FormTarget target = getAttribute(attributes.target);
}

These classes can be instantiated very easily:

Div
{
	object attributes
		satisfies Attributes
	{
		classes = ["form-container", "foo-bar"];
		lang = "en";
	}
	
	Form
	{
		object attributes
			satisfies Attributes
		{
			classes = ["my-form"];
			id = "main-form";
		}
		
		P
		{
			Label{ "Login", Input {}}
		},
		P
		{
			Label
			{
				"Password",
				Input
				{
					object attributes
						satisfies InputAttributes
					{
						type = password;
					}
				}
			}
		},
		P
		{
			Button { "Submit" }
		}
	}
}

To instantiate look‐alike elements, one can refine the non‐defaulted attributes, and make the changeable ones variable.

object fakeBuilder
	satisfies Attributes
{
	classes = ["foo-container", "pretty"];
	dir = rtl;
	shared actual variable Attribute<String> id = super.id;
}

fakeBuilder.id = "pretty1";

value div1 = Div {attributes = fakeBuilder;};

fakeBuilder.id = "pretty2";

value div2 = Div {attributes = fakeBuilder;};

A class can make an attribute unspecifiable by refining it as non‐refinable in its own Attributes interface.


Of course, I am not going to deny that writing Div { lang = "en"; "Hello" } looks much nicer than writing Div { object attributes satisfies Attributes { lang = "en"; } "Hello" }, however, as shown here, the later, while more verbose, is much more flexible for both the writer of the class Div and for its users.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions