In the Sightly templating language, for Adobe AEM6 (CQ), how do I add an attribute to an element only if a condition is true, without duplicating lots of code/logic?
e.g.
<ul data-sly-list="${items}" ${if condition1} class="selected"${/if}>
<li${if condition2} class="selected"${/if}>
Lots of other code here
</li>
</ul>
When setting HTML attributes dynamically (with an expression), Sightly guesses your intention to simplify the writing:
If the value is an empty string or if it is the false boolean, then the attribute gets remove altogether.
For instance <p class="${''}">Hi</p> and <p class="${false}">Hi</p> render just <p>Hi</p>.
If the value is the true boolean, then the attribute is written as a boolean HTML attribute (i.e. without attribute value, like for e.g. the checked, selected, or disabled form attributes).
For instance <input type="checkbox" checked="${true}"> renders <input type="checkbox" checked>.
You can then use two Sightly operators to achieve what you want (both work as in JavaScript): the ternary conditional operator, or the logical AND (&&) operator.
Ternary conditional operator
<ul data-sly-list="${items}" class="${condition1 ? 'selected' : ''}">
<li class="${condition2 ? 'selected' : ''}">
Lots of other markup here
</li>
</ul>
Logical AND operator
For that, you additionally have to understand that like in JavaScript, ${value1 && value2} returns value1 if it is falsy (e.g. false, or an empty string), otherwise it returns value2:
<ul data-sly-list="${items}" class="${condition1 && 'selected'}">
<li class="${condition2 && 'selected'}">
Lots of other markup here
</li>
</ul>
As said, in both examples the class attribute will be removed altogether if the corresponding condition is false.
What Gabriel has said is entirely correct. I did want to add a "gotcha" to look out for though. For the record, I was running into the exact same problem where, in a Sightly template, I wanted to toggle the presence of an input element's "checked" attribute based on a boolean value. In my case this boolean value was coming from the backing Use class.
After about 3-4 hours and being on the verge of pulling my hair out I finally realized that the boolean value I was relying on for toggling the "checked" attribute was ultimately being set in the activate method of a Sling service I wrote to back the work I'm doing. While everything else was logically correct, because I was setting this boolean in activate() and then retrieving it via a getter as needed, it would only ever have its value updated on bundle activation meaning my view would essentially lose state after the first refresh of the page.
I know it's silly but I wanted to call it out since it's relevant here and it may help someone not have to lose some hair like I did...
Related
I've seen a ton of examples related to conditional attributes, they all talk about this:
<img data-srcset="${myModel.isPrimaryImg ? '/img/image1.jpg': '/img/image2.jpg'}" >
This either renders
<img data-srcset="/img/image1.jpg" >
Or
<img data-srcset="/img/image2.jpg" >
Depeneding on whether ${myModel.isPrimaryImg} is true or false respectively
this is not what I want, the above example deals with the "value" part of the attribute
I want to put the ternery condition on the "Key" part of the attribute as below:
<img ${myModel.isTrue ?'data-srcset=' : 'srcset='} = "/img/common-image.jpg" >
And I would expect it to render
Either
<img data-srcset= "/img/common-image.jpg" >
Or
<img srcset= "/img/common-image.jpg" >
This was possible using JSP as much as I remember
The only way I can expect it to work as I want is if I add the condition like this:
<img data-sly-test="${myModel.isTrue}" data-srcset = "/img/common-image.jpg" ><!-- When TRUE -->
<img data-sly-test="${!myModel.isTrue}" srcset = "/img/common-image.jpg" >!-- When FALSE -->
But this needs two whole tags to be present one for each condition
Is there a better way to put the ternary condition in HTL for data-attribute keys and not values ?
I have a lot of such tags so I was thinking if I could fit the condition in one line only or else I'll end up doubling the number of tags, where each tag will contain one attribute based on the value of the flag one for true and one for false condition.
You can either:
Use data-sly-attribute and expose the attributes as a map via a Use-Object object. You can then control the map keys in your Use-Object.
Take advantage of empty attributes not being rendered (spec) and do something like:
<img data-srcset="${myModel.isTrue ? '/img/common-image.jpg' : ''}" srcset="${!myModel.isTrue ? '/img/common-image.jpg' : ''}">
That is reasonably readable and will keep your HTL script HTML-valid.
In the Sightly templating language, for Adobe AEM6, how do I use a specific class if a condition is true?
${properties.reduceImage} is my checkbox, so if the checkbox is checked then add the class if not then it doesn't return anything. I'm not sure if I'm doing this the correct way.
<div data-sly-test="${properties.reduceImage}" data-sly-unwrap>
<div class="reduce-image">
</div>
</div>
The expression language of Sightly has a few operators that allow to do things like that.
In your case, you have two possibilities: the conditional operator, or the logical AND (&&) operator.
Conditional operator
This operator works like data-sly-test, but at the level of the expression. What is before the question mark (?) is the condition, and then come two parts, separated with a column (:). The first part is the output if the condition is true, and the second part is the output if the condition is false (which we leave empty in your example).
<div class="${properties.reduceImage ? 'reduce-image' : ''}">
</div>
Logical AND operator
This writing is a bit shorter, but also less explicit in it's intention. It uses the fact that like in JavaScript, ${value1 && value2} returns value1 if it is falsy (e.g. false, or an empty string), otherwise it returns value2:
<div class="${properties.reduceImage && 'reduce-image'}">
</div>
In both examples the class attribute will be removed altogether if the corresponding condition is false, because Sightly does remove all attributes with expression that end up being empty or false.
Here's the full documentation of Sightly's expression language:
http://docs.adobe.com/docs/en/aem/6-1/develop/sightly/expression-language.html
I'm working on a bigger project with AngularJS. Therefore, I want to make the work for a single form as easy as possible. As we're also using bootstrap, the code for a single input field in a form is quite verbose, maybe like
<div class="control-group">
<label class="control-label" for="inputEmail">Email</label>
<div class="controls">
<input type="text" id="inputEmail" placeholder="Email">
</div>
</div>
If I could write a single tag like
<custom-input
label="Email"
name="inputEmail"
placeholder="Email"
type="text"
... >
</custom-input>
instead, this would help to keep the code clean and the work simple.
To achive this, I'm working on a custom AngularJS directive. My directive currently uses a template similar to the bootstrap example from above, automatically assigning the label to the input-tag. Also, the directive's compiler function moves all attributes from the custom-input tag to the real input-tag in order to make it easy to customize the custom-input tag.
app.directive('customInput', function() {
return {
require: 'ngModel',
restrict: 'E',
template: '<div>' +
'<label for="{{ name }}">the label</label>' +
'<input id="{{ name }}" ng-model="ngModel" />' +
'</div>',
scope: {
ngModel: '=',
name: '#name',
},
replace: true,
compile: function (tElement, tAttrs, transclude) {
var tInput = tElement.find('input');
// Move the attributed given to 'custom-input'
// to the real input field
angular.forEach(tAttrs, function(value, key) {
if (key.charAt(0) == '$')
return;
tInput.attr(key, value);
tInput.parent().removeAttr(key);
});
return;
},
};
});
On Stack Overflow, there are many questions regarding the creation of custom input fields, but they are concerned with the data binding, custom formatting or binding to ng-repeat.
My approach however has a different issue: while the data binding works correctly, Angular's integrated form validation module get confused when the input field is 'required'. For some reason, validation doesn't recognize the new input field and instead keeps the form invalid because of some dead reference, which has an empty value. Please see the minimal example.
Where does the dead reference come from? How can I update the validation-module's references? Is there a better way to achieve my overall goal?
As a boolean attribute, there is a corresponding required property that is still true on your div even if the attribute is moved.
The required attribute isn't getting moved, it must be getting skipped because there is no value. I don't know how to even add it to an element using javascript without a value, but using the form required="required" fixes that
Using transclude=true will use a copy of your element after the compile phase when you moved attributes, I think this keeps the required property from being set
You have to assign a higher priority for some reason, maybe because of ng-model, which is not removed from your div because the name in tattrs is ngModel (although removing from the div doesn't remove the need for priority)
http://plnkr.co/edit/5bg8ewYSAr2ka9rH1pfE?p=preview
All I did was change the required attribute to be required="required" and add these two lines to the directive declaration:
transclude: true,
priority: 10,
I put ng-transclude on the template label by the way so the contents of your element will go in the label and you don't have to have an attribute for that.
This application is for running a writing contest.
Coodinators are assigning entries to judges for them to judge. I have three sets of data I retrieve from the server, a judge list, an entries list and an assignment list that ties the two together. There can be a variable number of input fields...if a judge has agreed to judge 4 entries, there will be 4 inputs...if 7, then 7.
I have all of that working OK, but only insofar as the entry number can be input and the data updated.
Now I would like confirm that the entryID IS a valid ID by checking the list and also to show a field or two on the screen so the coordinator knows that they typed in the right entry.
The relevant section of the HTML
<div ng-app>
<div id="assignment" ng-controller="AssignData" ng-init="JudgeID=107;CategorySelect='MS';PublishSelect='P'">
<div ng-show="loaded">
<form class="entryform ng-cloak" name="assignform" ng-submit="sendForm()">
<p>Entry numbers assigned to this judge</p>
<p ng-repeat="assign in (formassigns =(assigns | filter:AssignedJudge))">
<input type="text" ng-model="assign.entryid" required/>
{{entries.authorname}} {{entries.entrytitle}}
</p>
<button type="submit">Save Assignments</button>
<p>This will keep the assignments attached to this judge.
You will be able to send all of your assignments to all
of your judges when you are finished.</p>
</form>
</div>
</div>
</div>
The part that I haven't been able to figure out is how to make entries.authorname and entries.entrytitle show up when the user types in an entryid that is in entries.entryid.
assigns and entries are both arrays of records using JSON
assigns is JSON made up of assigns.id, assigns.judgeid, assigns.entryid.
entries is JSON made up of entries.entryid, entries.entrytitle, entries.authorname
When assigns arrives, entryid is empty. The form is used to fill in the entryid and when it is filled in, I'd like to be able to show next to it the title and authorname for that entry.
NOTE: I've added some important information at the end of this answer. So please read to the end before you decide what you're going to do.
You're going to have to do something that does the look up.
Also a few other changes I'd add, mostly so you can actually validate the items in your repeat.
(There's a summary of what I did after the psuedo code below).
<div ng-app>
<div id="assignment" ng-controller="AssignData"
ng-init="JudgeID=107;CategorySelect='MS';PublishSelect='P'">
<div ng-show="loaded">
<form class="entryform ng-cloak" name="assignform" ng-submit="sendForm()">
<p>Entry numbers assigned to this judge</p>
<p ng-repeat="assign in (formassigns =(assigns | filter:AssignedJudge))"
ng-form="assignForm">
<input type="text" ng-model="assign.entryid"
ng-change="checkEntryId(assign, assignForm)"
name="entryid" required/>
<span ng-show="assignForm.entryid.$error.required">required</span>
<span ng-show="assignForm.$error.validEntry">
{{assignForm.$error.validEntry[0]}}</span>
{{assign.entry.authorname}} {{assign.entry.entrytitle}}
</p>
<button type="submit">Save Assignments</button>
<p>This will keep the assignments attached to this judge.
You will be able to send all of your assignments to all
of your judges when you are finished.</p>
</form>
</div>
</div>
</div>
Then in your controller, you'd add a function like so (be sure to inject $http or a service you wrote to pull the values from the server):
$scope.checkEntryId = function(assign, form) {
$http.get('/CheckEntry?id=' + assign.entryid,
function(entry) {
if(entry) {
assign.entry = entry;
form.$setValidity('validEntry', true);
} else {
form.$setValidity('validEntry', false, 'No entry found with that id');
}
}, function() {
form.$setValidity('validEntry', true, 'An error occurred during the request');
console.log('an error occurred');
});
};
The basic idea above:
Use ng-form on your repeating elements to allow for validation of those dynamic parts.
Create a function that you can pass your item and your nested form to.
In that function, make your AJAX call to see if the entry is valid.
Check the validity based on the response, and call $setValidity on your nested form you passed to the function.
Use ng-show on a span (or something) in your nested form to show your validation messages.
Also, assign your checked entry to your repeated object for display purposes. (you could use a seperate array if you want, I suppose, but that would probably get unnecessarily complicated).
I hope that helps.
EDIT: Other thoughts
You might want to wrap your call in a $timeout or some sort of throttling function to prevent the entry id check from spamming yoru server. This is an implementation detail that's totally up to you.
If this is a check you do all over the place, you'll probably want to create a directive to do it. The idea would be very similar, but you'll do the check inside of a $parser on the ngModelController.
The method I showed above will still actually update the model's entryid, even if it's invalid. This is usually not a big deal. If it is, you'll want to go with what I suggested in "other thought #2", which is a custom validation directive.
If you need more information about validation via custom directives I did a blog entry on that a while back
I have the following Sightly expression:
<li data-sly-call="${linkTemplate.dynamicLink # section='education',
url='/en/life-career-events.html', text=${'comp.masthead.navigation.home' # i18n}}">
</li>
The dynamiclink template is as follows:
<div data-sly-template.dynamicLink="${# section, url, text}"
data-sly-use.membersNav="${'com.comp.cms.component.masthead.MembersNavigation' # section=section}">
<a data-sly-attribute.class="${membersNav.cssClass}" href="${url}">${text}</a>
</div>
This doesn't work because text=${'comp.masthead.navigation.home' # i18n} isn't evaluated as a string and then passed into the dynamiclink.
Is this possible? Can I evaluate and assign to a variable or do I have to create a new template when I want to evaluate i18n lookups?
Sightly 1.1 doesn't allow to have expressions within expressions, and there's no plan to change that for now.
Hacking the solution:
There's a trick: data-sly-test can be (ab)used to set variables. It's not really a recommended way to do though, unless you have a real condition, because this will mislead someone who reads the template in thinking that the intention was to have an actual condition.
The trick goes like that: an identifier can be provided to data-sly-test, which will expose the result of the test as a variable. Additionally, data-sly-test will be considered true, unless the resulting string is empty.
For e.g.:
<p data-sly-test.spanishAsset="${'Asset' # i18n, locale='es'}">${spanishAsset}</p>
Outputs:
<p>Recurso</p>
So in your case, you could write:
<li data-sly-test.linkText="${'comp.masthead.navigation.home' # i18n}"
data-sly-call="${linkTemplate.dynamicLink # section='education',
url='/en/life-career-events.html', text=linkText}">
</li>
A cleaner solution
As you probably don't want to explain to all the users of this template that they have to write such a hack, and instead of having two separated templates for translated and non-translated texts, you could instead leverage optional templates parameters. So you might for e.g. have a noI18n optional parameter.
The call would then be as simple as it can be:
<!--/* Translating would be the default behavior */-->
<li data-sly-call="${linkTemplate.dynamicLink #
section='education',
url='/en/life-career-events.html',
text='comp.masthead.navigation.home'}"></li>
<!--/* Or translating could be turned off */-->
<li data-sly-call="${linkTemplate.dynamicLink #
section='education',
url='/en/life-career-events.html',
text='my text...',
noI18n=true}"></li>
The template would then have two data-sly-test conditions for the two cases (note that the data-sly-unwrap attributes can be dropped in AEM 6.1+):
<div data-sly-template.dynamicLink="${# section, url, text, noI18n}"
data-sly-use.membersNav="${'com.comp.cms.component.masthead.MembersNavigation'
# section=section}">
<a href="${url}" data-sly-attribute.class="${membersNav.cssClass}">
<sly data-sly-test="${noI18n}" data-sly-unwrap>${membersNav.text}</sly>
<sly data-sly-test="${!noI18n}" data-sly-unwrap>${membersNav.text # i18n}</sly>
</a>
</div>
Optionally, to keep the template as simple as possible and to remove those conditions, you could also make the Use-API do the translation, depending on the noI18n optional parameter:
<div data-sly-template.dynamicLink="${# section, url, text, noI18n}"
data-sly-use.membersNav="${'com.comp.cms.component.masthead.MembersNavigation'
# section=section, noI18n=noI18n}">
<a href="${url}" data-sly-attribute.class="${membersNav.cssClass}">
${membersNav.text}
</a>
</div>
The proper code for the logic to translate a string is:
Locale pageLang = currentPage.getLanguage(false);
I18n i18n = new I18n(slingRequest.getResourceBundle(pageLang));
String text = i18n.get("Enter a search keyword");