jte icon indicating copy to clipboard operation
jte copied to clipboard

Conditional Attributes

Open tschuehly opened this issue 1 year ago • 10 comments

Currently I cannot use a @if conditional around a html attribute

<div @if(comment.parentComment() != null)
            x-init="$dispatch('${comment.parentComment().toString()}')"
        @endif
>

I get the following exception:

CommentComponent.jte, error at line 9: Illegal HTML attribute name @if(comment.parentComment()! @if expressions in HTML attribute names are not allowed. In case you're trying to optimize the generated output, smart attributes will do just that: https://jte.gg/html-rendering/#smart-attributes

I really would like to do that as I don't necessarily want to put my alpine.js code into my java code and instead put it into my template.

tschuehly avatar Sep 26 '24 11:09 tschuehly

If the attribute value is null, it will be omitted, hope that helps.

kelunik avatar Sep 26 '24 17:09 kelunik

If the attribute value is null, it will be omitted, hope that helps.

Yes I know that but I would need to put the if else in a ternary operator which is uglier imo.

@casid If you think this is ok/possible I could take a crack at changing the behaviour. Or is there a certain reason why the behaviour is as it is?

tschuehly avatar Sep 28 '24 06:09 tschuehly

@tschuehly this is because jte by default only allows writing into safe slots of a template. The parser expects to find an attribute name within this div. This needs to be done in order to decide how to escape the attribute content, which is a safe slot to write to. Having logical expressions in here would let the complexity of the parser explode, and also make it impossible to guarantee properly escaped templates.

casid avatar Oct 02 '24 02:10 casid

@tschuehly this is because jte by default only allows writing into safe slots of a template. The parser expects to find an attribute name within this div. This needs to be done in order to decide how to escape the attribute content, which is a safe slot to write to. Having logical expressions in here would let the complexity of the parser explode, and also make it impossible to guarantee properly escaped templates.

Is there a way to extend the parser? So I could write a plugin where I could implement that behaviour?

tschuehly avatar Oct 09 '24 14:10 tschuehly

No there is not. The parser is already quite complex and I fear opening this rabbit hole would make everything a lot harder to maintain, while the benefit to it is rather low. The risk on the other hand quite high, to accidentially introduce unsafe behavior or break user code.

casid avatar Oct 09 '24 17:10 casid

How about using a special constant OMIT that would remove the attribute from the output if it is encountered? Similar to the poison pill pattern. Then you could write it like this.

<div x-init="comment.parentComment() == null ? OMIT : $dispatch('${comment.parentComment().toString()}')">

leonard84 avatar Oct 31 '24 12:10 leonard84

This constant already exists: null

kelunik avatar Oct 31 '24 21:10 kelunik

I stumbled upon the same issue and I'm not able to find a working solution. I tried following variants:

  1. @if inside the attribute
<input id="${field.id()}" name="${field.name()}" type="${field.type()}"
	value="${field.stringValue()}"
	required="${field.validation().notNull()}"
	oninvalid="@if (field.validation().notNull()) this.setCustomValidity('${field.validation().notNullMessage()}') @endif"
	oninput="@if (field.validation().notNull()) this.setCustomValidity('') @endif"/>

which gives the following output when the field is false

<input id="name" name="name" type="TEXT" value="" oninvalid="" oninput="">

and this output when the field is true

<input id="name2" name="name2" type="TEXT" value="" required="" oninvalid=" this.setCustomValidity('Please provide a name') " oninput=" this.setCustomValidity('') ">
  1. Ternary operator in 'java' code:
<input id="${field.id()}" name="${field.name()}" type="${field.type()}"
	value="${field.stringValue()}"
	required="${field.validation().notNull()}"
	oninvalid="${field.validation().notNull() ? "this.setCustomValidity('${field.validation().notNullMessage()}')" : null}"
	oninput="${field.validation().notNull() ? "this.setCustomValidity('')" : null}"/>

which gives the following output when the field is false

<input id="name" name="name" type="TEXT" value="" oninvalid="">

and this output when the field is true

<input id="name" name="name" type="TEXT" value="" required="" oninvalid="this.setCustomValidity(\x27\x24{field.validation().notNullMessage()}\x27)" oninput="this.setCustomValidity(\x27\x27)">

I was trying to come up with another solution, but I was not able to find it.

Could you please help me modify my code so that it gives me the desired result - when the field is false I expect the attributes to be omitted:

<input id="name" name="name" type="TEXT" value="">

and when the field is true I expect the attributes to have correctly escaped the values:

<input id="name2" name="name2" type="TEXT" value="" required="" oninvalid="this.setCustomValidity('Please provide a name')" oninput="this.setCustomValidity('')">

The 1. solution is the closest to the desired result, but the attributes are there with empty values when I expect them to be gone and when they're present they have empty spaces before and after (most probably is the first space after @if (field.validation().notNull()) and the second the space before @endif).

vasekbrychta avatar Mar 25 '25 21:03 vasekbrychta

@vasekbrychta Usually I use a dedicated method like you did for some attributes. For example:

<input id="${field.id()}" name="${field.name()}" type="${field.type()}"
	value="${field.stringValue()}"
	required="${field.required()}"
oninvalid="${field.onInvalid("this.setCustomValidity('${field.validation().notNullMessage()}')"}"
	oninput="${field.onInput("this.setCustomValidity('')")}"/>

This way you condition is directly in your Java model. This makes reading even easier.

laurentpellegrino avatar Mar 25 '25 22:03 laurentpellegrino

I don't think I follow - let's take the most complicated case of oninvalid="this.setCustomValidity('Please provide a name')":

  1. the part Please provide a name cannot be trusted and needs to be escaped properly (that's the part field.validation().notNullMessage() that can contain anything from the user)
  2. the part this.setCustomValidity('...') is a trusted javascript call inside the template that should take the 1. part as an argument

I tried your solution oninvalid="${field.onInvalid("this.setCustomValidity('${field.validation().notNullMessage()}')"}" with onInvalid method being

public String onInvalid(String s) {
	if (required)
		return s;
	return null;
}

and the result is oninvalid="" when the method returns null and oninvalid="this.setCustomValidity(\x27\x24{field.validation().notNullMessage()}\x27)" otherwise. The oninput attribute is properly gone when null is returned - I have no clue what is the difference between these two cases. But I might miss some important part, this is my first day experimenting with JTE.

vasekbrychta avatar Mar 25 '25 22:03 vasekbrychta