-
Notifications
You must be signed in to change notification settings - Fork 60
change pattern for attributes in ceylon.html
#697
Comments
I think that's a deal breaker. Having to write Normally in Ceylon, I try to use A few other notes:
|
@jvasileff, to address a specifically your “serialization of attributes” concern: I have started implementing my idea, and the way I’ve dealt with this is by:
Since I probably didn’t do a good job explaining, here is how I have the "Represents base class for all HTML elements."
shared sealed abstract class Element(tagName, attributes, children)
extends Node() {
"The name of the tag for this element."
shared String tagName;
"The attributes associated with this element."
shared restricted Attributes attributes;
"The children of this element."
shared {Content<Node>*} children;
shared restricted default{<String->String>*} serializedAttributes => serializeAttributes(attributes);
}
{<String->String>*} serializeAttributes(Attributes attributes) => {
"id"->attributes.id,
"class"->serializeSequentialAttribute(attributes.classes),
"accesskey"->serializeSequentialCharacterAttribute(attributes.accessKey),
"contenteditable"->serializeInheritableBooleanAttribute(attributes.contentEditable),
"contextmenu"->attributes.contextMenu,
"dir"->serializeOptionalAttribute(attributes.dir),
"draggable"->serializeInheritableBooleanAttribute(attributes.draggable),
"dropzone"->serializeOptionalAttribute(attributes.dropZone),
"hidden"->serializeBooleanAttribute(attributes.hidden),
"lang"->attributes.lang,
"spellcheck"->serializeInheritableBooleanAttribute(attributes.spellcheck, "yes", "no"),
"style"->serializeNonemptyAttribute(attributes.style),
"tabindex"->serializeIntegerAttribute(attributes.tabIndex),
"title"->attributes.title,
"translate"->serializeInheritableBooleanAttribute(attributes.translate, "yes", "no"),
*attributes.more
}.map(coalesceItem).coalesced; Where shared interface Attributes {
shared default String? id => null;
shared default [String*] classes => [];
shared default [Character*] accessKey => [];
shared default Boolean? contentEditable => null;
shared default String? contextMenu => null;
shared default Direction? dir => null;
shared default Boolean? draggable => null;
shared default DropZone? dropZone => null;
shared default Boolean hidden => false;
shared default String? lang => null;
shared default Boolean? spellcheck => null;
shared default String style => "";
shared default Integer? tabIndex => null;
shared default String? title => null;
shared default Boolean? translate => null;
shared default {<String->String>*} more => [];
} An element that doesn’t define unique attribute doesn’t need to worry about refining tagged("flow", "phrasing")
shared final class Abbr(attributes = object satisfies Attributes {}, children = [])
extends Element("abbr", attributes, children)
satisfies FlowCategory & PhrasingCategory {
"The attributes associated with this element."
Attributes attributes;
"The children of this element."
{Content<Node>*} children;
} An element that has unique attributes like tagged("flow", "phrasing", "embedded", "interactive")
shared final class Audio(attributes = object satisfies AudioAttributes {}, children = [])
extends Element("audio", attributes, children)
satisfies FlowCategory & PhrasingCategory & EmbeddedCategory & InteractiveCategory {
"The attributes associated with this element."
AudioAttributes attributes;
"The children of this element."
{Content<Source|Track|FlowCategory>*} children;
shared restricted actual {<String->String>*} serializedAttributes => {
"autoplay"->serializeBooleanAttribute(attributes.autoplay),
"controls"->serializeBooleanAttribute(attributes.controls),
"loop"-> serializeBooleanAttribute(attributes.loop),
"muted"->serializeBooleanAttribute(attributes.muted),
"preload"->serializeOptionalAttribute(attributes.preload),
"src"->attributes.src,
*super.serializedAttributes
}.map(coalesceItem).coalesced;
}
shared interface AudioAttributes
satisfies Attributes {
shared default Boolean autoplay => false;
shared default Boolean controls => false;
shared default Boolean loop => false;
shared default Boolean muted => false;
shared default Preload? preload => null;
shared default String? src => null;
} If there is a hierarchy between two So, we need to separate it into its own declaration: tagged("flow", "phrasing", "interactive")
shared final class A(attributes = object satisfies AAttributes {}, children = [])
extends Element("a", attributes, children)
satisfies FlowCategory & PhrasingCategory & InteractiveCategory {
"The attributes associated with this element."
AAttributes attributes;
"The children of this element."
{Content<FlowCategory>*} children;
shared restricted actual {<String->String>*} serializedAttributes => serializeAAttributes(attributes);
}
shared interface AAttributes
satisfies BaseAttributes {
shared default String|Boolean download => false;
shared default String? hreflang => null;
shared default [String*] rel => [];
shared default MimeType? type => null;
}
{<String->String>*} serializeAAttributes(AAttributes attributes) => {
"download"->(switch(attribute = attributes.download) case(is String) attribute case(is Boolean) serializeBooleanAttribute(attribute)),
"hreflang"->attributes.hreflang,
"rel"->serializeSequentialAttribute(attributes.rel),
"type"->serializeOptionalAttribute(attributes.type),
*serializeAttributes(attributes)
}.map(coalesceItem).coalesced; Then, we can use it from tagged("flow", "phrasing")
shared final class Area(attributes = object satisfies AreaAttributes {}, children = [])
extends Element("area", attributes, children)
satisfies FlowCategory & PhrasingCategory {
"The attributes associated with this element."
AreaAttributes attributes;
"The children of this element."
{Content<PhrasingCategory>*} children;
shared restricted actual {<String->String>*} serializedAttributes => {
"alt"->attributes.alt,
"coords"->serializeSequentialIntegerAttribute(attributes.coords),
"media"->attributes.media,
"shape"->serializeOptionalAttribute(attributes.shape),
*serializeAAttributes(attributes)
}.map(coalesceItem).coalesced;
}
shared interface AreaAttributes
satisfies AAttributes {
shared default String alt => "";
shared default [Integer*] coords => [];
shared default String? media => null;
shared default Shape? shape => null;
} By the way, the definition of String? serializeSequentialAttribute([String*] attribute) {
if(nonempty attribute) {
return " ".join(attribute);
}
else {
return null;
}
}
<Key->Item&Object>? coalesceItem<out Key, out Item>(Key->Item element)
given Key satisfies Object {
if(exists item = element.item) {
return element.key->item;
}
else {
return null;
}
} It’s also interesting to note that, since streams are lazy, I don’t need to worry about having an value div = Div
{
object attributes
satisfies Attributes
{
classes => potentiallyHeavyComputation();
// or even:
// shared actual late [String*] classes = potentiallyHeavyComputation();
}
}; In case you are wondering why I have put
That’s a bummer, considering that one might want to instantiate, say, an
That’s an interesting idea, but I feel like it’s overall worse because it requires verbose boilerplate for every element, and not only elements with attributes. Additionally, I’d say that an element’s tag is more important semantically than its attributes, so having noise in there makes the templates harder to read than in the attributes. |
So, this is library developer ease of use versus end-user ease of use question. The way I see this. @Zambonifofex, I see your pain and I understand it, but forcing library users to write their declarative HTML like this is unacceptable: 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" }
}
}
}```
When currently we can do this (I hope I got it right):
```ceylon
Div {
classes = ["form-container", "foo-bar"];
lang = "en";
Form {
classes = ["my-form"];
id = "main-form";
P {
Label{ "Login", Input() }
},
P {
Label{ "Password", Input(type = password) }
},
P {
Button { "Submit" }
}
}
}
}``` |
So, something occurred to me. @gavinking’s “inline object You’ll see me arguing against the feature there, but it seems it would actually be more generally useful than what I could first see. (It wouldn’t be only useful for this particular idea.) Then, you could shorten it down to: value section = Section
{
object attributes {id = "how-it-works"; classes = ["foo-bar"];}
H1 {"How it works"},
P {"..."}
}; Or even: value section = Section
{
object {id = "how-it-works"; classes = ["foo-bar"];};
H1 {"How it works"},
P {"..."}
}; Of course, ideally |
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
). Forceylon.html
, it ought to look something like the following.Then, the
Element
class can look like: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.However, subclasses that do need to declare more attributes can define a subtype of
Attributes
and specify them there.These classes can be instantiated very easily:
To instantiate look‐alike elements, one can refine the non‐defaulted attributes, and make the changeable ones
variable
.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 writingDiv { 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 classDiv
and for its users.The text was updated successfully, but these errors were encountered: