Giter VIP home page Giter VIP logo

Comments (4)

jvasileff avatar jvasileff commented on July 18, 2024

writing ... looks much nicer than writing ...

I think that's a deal breaker. Having to write object attributes satisfies XAttributes {...} for tags w/attributes is way too verbose and detracts from the content.

Normally in Ceylon, I try to use formal attribute in base classes, which among other benefits reduces the size of the extends clause. But I think one reason this is not done in ceylon.html is that attributes must be generically enumerated during serialization, so they are stored in a Sequential in the base class. Perhaps I missed it, but I don't see the enumeration concern being addressed by the Attributes classes.

A few other notes:

  • For API evolution, I think you need to use abstract classes. I don't think the current compiler supports introducing new defaulted interface members without recompiling satisfying classes.
  • Remember, interfaces attributes must use =>
  • I'm not sure how efficient it would be to declare new classes for each use of a tag (OTOH I guess the compiler declares so many classes anyway for lazy iterables)
  • Perhaps you could do something like object extends Div { id="1"; ... } w/o separate attribute classes, but that only helps a little with the verbosity, and enumerability of attributes must still be addressed. And, I guess, tags would need no-arg constructors.

from ceylon-sdk.

zamfofex avatar zamfofex commented on July 18, 2024

@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:

  • making element classes final,
  • making the Element class sealed,
  • adding a serializedAttributes attribute to them,
  • adding unshared serializeXxxAttribute functions that can serialize different kinds of attributes (e.g. Boolean, Sequence, AttributeValueProvider),
  • for XAttributess that have subtypes, adding a serializeXAttributes to allow the associated X class to serialize them,
  • for X classes whose XAttributess don’t have subtypes, merely use super.serializeAttributes.

Since I probably didn’t do a good job explaining, here is how I have the Element.ceylon unit:

"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 Attributes is (sans the documentation):

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 attributes, as it’s the case of Abbr:

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 Audio needs to refine attributes:

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 XAttributes interfaces, such as the case of AAttributes and AreaAttributes, then the serialization of AAttributes cannot be done inside A, because Area doesn’t inherit from A, and so it can’t use super.serializedAttributes to refer to A.

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 Area:

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 serializeXxxAttribute should be clear. Here is an example to demonstrate:

String? serializeSequentialAttribute([String*] attribute) {
    if(nonempty attribute) {
        return " ".join(attribute);
    }
    else {
        return null;
    }
}

coalesceItem (probably a misnomer), is defined as:

<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 Attribute alias, since people who want their attributes to be only evaluated at template rendering time can write the attributes with => instead of with =:

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 serializedAttributes in X instead of XAttributes, it’s because I don’t think it’s a good idea that you can end up with href="https://whatever/" in, say, a Div.


For API evolution, I think you need to use abstract classes. I don't think the current compiler supports introducing new defaulted interface members without recompiling satisfying classes.

That’s a bummer, considering that one might want to instantiate, say, an A and an Input with the same global attributes set.

Perhaps you could do something like […]

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.

from ceylon-sdk.

luolong avatar luolong commented on July 18, 2024

So, this is library developer ease of use versus end-user ease of use question.

The way I see this. ceylon.html biggest strength so far is that it is a fairly close to 1:1 parity between HTML format and Ceylon declarative code style.

@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" }
        }
    }
}
}```

from ceylon-sdk.

zamfofex avatar zamfofex commented on July 18, 2024

So, something occurred to me. @gavinking’s “inline object satisfies clause inferrence” idea (eclipse-archived/ceylon#5739) could help us out here.

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 {} wouldn’t have been taken by streams, so that the object keyword could have been omitted (just like function can be omitted in lambdas), but that’s just not how the language has evolved to be.

from ceylon-sdk.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.