Widget dialogue that creates new nodes - Adobe CQ5 - servlets

I'm trying to create an edit widget for a page that asks for various details and then stores those details as properties of a newly created node.
So for instance:
Make
Model
Mileage
and I want to store those as a node under a parent node in /content/cars or something similar.
How can I get a widget to create a new node under /content/cars and store those properties there?
Do I have to send the data to a servlet that I create? Or are there options to enable this?

What you'd probably want to do is include a component in the containing page at the path you want the nodes created.
For example, if you have a page /content/cars, and you wanted to create a Chevy node under there at /content/cars/jcr:content/chevy, you'd include the component with this fragment:
<cq:include path="chevy" resourceType="myapp/components/manufacturer" />
This would include the content and render it and allow it to be edited with the component located at /apps/myapp/components/manufacturer.
If you want you can even have the node be at an absolute path, for example if you want to reference the content at /content/data/cars/chevy on another page you could use:
<cq:include path="/content/data/cars/chevy" resourceType="myapp/components/manufacturer" />

Based on the provided cheatsheet from Adobe:
http://dev.day.com/content/ddc/blog/2008/07/cheatsheet/_jcr_content/par/download/file.res/cheatsheet.pdf
The default Sling POST servlet recognizes what JCR properties to update by examining the 'name' attribute of the input tags that are included in the ExtJS dialogs. To configure a widget like a textfield or dropdownfield to target and update a specific property, you will need to specify the property's name in the widget definition.
For example, if you are using an XML file to define your page dialog at /content/cars and you wished for there to be widgets that let users store model, make, and year to the jcr:content node of the cars page. Your associated dialog definition would contain nodes that look like:
<make
jcr:primaryType="cq:Widget"
xtype="textfield"
name="./make"/>
<model
jcr:primaryType="cq:Widget"
xtype="textfield"
name="./model"/>
<year
jcr:primaryType="cq:Widget"
xtype="textfield"
name="./year"/>
To change the widget type you would adjust the xtype attribute and to change the property to target you would adjust the name attribute. Note that it is relative from the content node of the page/component where the dialog is being invoked (in this case, relative from /content/cars/jcr:content).
Another example: say you wanted to store the value of make to a property called 'make' on a node like /content/cars/jcr:content/brands, then your definition would look like:
<make
jcr:primaryType="cq:Widget"
xtype="textfield"
name="./brands/make"/>
For a reference on available xtypes, check out:
http://dev.day.com/docs/en/cq/current/widgets-api/index.html

Try this :
Add this js function to your pages
function doOpenDlg(url, path) {
var d = CQ.WCM.getDialog(url);
var reloadPage = true;
if(d) {
if( reloadPage ) {
d.success = function(form, action) {
CQ.Util.reload(CQ.WCM.getContentWindow());
};
}
d.show();
d.loadContent(path);
}
}
and then create a dialog like file. In this exemple :
/apps/myapp/components/page/mypage/myxmlfile.xml
then you can open a dialog matching this file with :
<input type="button" value="Open my dialog" style="font-size: 14px;font-weight: bold;"
onclick="doOpenDlg('/apps/myapp/components/page/mypage/myxmlfile.infinity.json', '<%=currentNode.getPath() %>');" />
with this, your dialog data will be saved in the currentNode, but you can put any path you want.

Related

Thymeleaf custom dialect with fragments

In my Spring Boot 2 project I have Thymeleaf fragments to generate form elements. For example:
<input th:replace="component/admin_form :: text (formElement = ${vm.getElementStatus()}, class='css-class-a'))" />
The code above generates a complex div block with label, input field and error block.
I would like to simplify this syntax. My idea was to create a custom dialect with a custom tag and write this:
<admin-form:text value="${vm.getElementLastName()}" class="css-class-a"/>
The second one is easier to read, it clearly indicates for the designers that this is a special element. Besides this, it would be easier to change themes as I only need to change the concrete fragment location in the tag processor and not hundreds of th:replace value.
It is also important that I don't want to build the complex html layout in the tag processor just want to somehow import a fragment. So designers can modify the html fragment without java code changes.
I was able to create the custom dialect and create the custom tag that generates a html block:
#Override
protected void doProcess(ITemplateContext context, IProcessableElementTag tag, IElementTagStructureHandler structureHandler) {
final IModelFactory modelFactory = context.getModelFactory();
final IModel model = modelFactory.createModel();
model.add(modelFactory.createOpenElementTag("div", "class", "test"));
model.add(modelFactory.createText("This is my custom element"));
model.add(modelFactory.createCloseElementTag("div"));
structureHandler.replaceWith(model, false);
}
But I don't know how to import a fragment in my custom tag.
Is it possible anyhow?
The doProcess of your <admin-form:text> tag could create a dummy tag with a th:replace attribute that includes the "complex input" fragment:
Map<String, String> attributes = new HashMap<>();
attributes.put("th:replace", "/pathToMyFragment/complexInput::myfragname");
IOpenElementTag newTag = modelFactory.createOpenElementTag("div", attributes, AttributeValueQuotes.DOUBLE, false);
model.replace(0, newTag);
or something similar (take care of closing the tag and so on).
The result would be to replace
<admin-form:text/>
with
<div th:replace="/pathToMyFragment/complexInput::myfragname"></div>
which in turn would be processed into the final HTML.
If you need to keep the original tag attributes (like class="css-class-a") you can get them from the original tag with model.get(0).getAttributeMap() and add them as a local variable with structureHandler.setLocalVariable("originalAttributes", theAttributeMap); to use in the final fragment, for example.

AEM - Multifield Add Item by default

I am using multifield of type pathfield in my dialog. To add new path field, user has to click Add Item link.
Is it possible to display at least one path field in the dialog by default (without clicking Add Item)?
Yes, it is possible. There was a similar question for which I have answered it here.
However, for future references, providing the answer here too.
You can listen to the loadcontent event fired by the Multifield after the content has been loaded and use the addItem() method.
<paths
jcr:primaryType="cq:Widget"
fieldLabel="Select Paths"
name="./paths"
xtype="multifield">
<fieldConfig
jcr:primaryType="nt:unstructured"
xtype="pathfield" />
<listeners
jcr:primaryType="nt:unstructured"
loadcontent="function(field, record)
{
if(record.get('paths') == undefined)
{
field.addItem(); field.doLayout();
}
}" />
</paths>

html5smartimage : Using pathfield as source of image , by disabling drap-drop

I would like to use html5smartimage widget without letting the users upload images from their desktop and also avoid using the contentfinder. I created a dialog with a pathfield and a html5smartimage inputs. I have set the "allowUpload" to false. I want the user to input/pick the image using the pathfield component and then use it as source image reference to render the image in the html5smartimage widget. I have not been successful so far and any help/hint would be appreciated :). Here is what I have tried so far :
set the same "name" value to the pathfield and the fileReferenceParamter , hoping to pick up the users pathfield input into the smartimage, but the POST results two "./srcImageReference" parameters to be sent resulting in a pathfield change to a String [] on jcr node, thus concatenation of same paths each time.
I have gone through widgets.js to find a usable event function that gets called when a drap-drop is done , so I can simulate a similar one with the value from the pathfield, but I could not find any ..
Is extending the smartimage component and overriding the default drap-drop handlers the only option ? if so how do I go about it .
thanks
Viper
The html5smartimage has a handleDrop( Object dragData ) method that is called when an image is dragged and dropped over it. We can call this from the dialogselect event of the pathfield.
In essence it's like faking a drag-drop. The dragData has a lot of fields but all that matters in this case is the image path and name (source : console.log()s in the handleDrop method ). So on when an image path is selected in the pathfield, use it's path and create hoax dragData object and call the image's handleDrop method.
function(pathfield,path,anchor){
var dialog = pathfield.findParentByType('dialog');
var image = dialog.findByType('html5smartimage')[0];
var pathArray = path.split('/');
var imageName = pathArray[(pathArray.length-1)];
var fakeDragData = {};
var fakeRecord = {};
fakeRecord.path = path;
fakeRecord.name = imageName;
fakeRecord.get = function(name){ return this[name]; };
fakeDragData.records = new Array(fakeRecord);
fakeDragData.single = true;
image.handleDrop(fakeDragData);
}
This way all of the images data is stored on node by the html5smart image widget itself, you will not need to read the value of pathfield in your jsp at all.
If you just want to use the html5smartimage to preview the image selected in the pathfield you can try something like this :
Instead of html5smartimage use a regular panel and use its html property to display the image in the panel. The panel's body.update() method can be called from listeners in the pathfield with markup up to create a preview image.
<tab1 jcr:primaryType="cq:Panel" title="Tab 1">
<items jcr:primaryType="cq:WidgetCollection">
<ImagePathField jcr:primaryType="cq:Widget" fieldLabel="Select Image"
name="./imagePath" rootPath="/content/dam" xtype="pathfield">
<listeners jcr:primaryType="nt:unstructured"
dialogselect="function(pathfield,currentpath,anchor) {
//dialogselect event - fired when a selection
//is made using pathfield's browse dialog
//get the sibling panel through the pathfield
var previewPanel = pathfield.nextSibling();
//update the html property of panel to contain the
//image selected in the pathfield using panel body's update method
previewPanel.body.update('<img style="max-width: 100%;display: block;
margin-left:auto;margin-right:auto;"
src="'+currentpath+'"/>');
}"
loadcontent="function(pathfield,record,path){
//preview the image when dialog is opened for the first time
var previewPanel = pathfield.nextSibling();
if(pathfield.getValue() != '')
previewPanel.body.update('<img style="max-width: 100%;display: block;
margin-left:auto;margin-right:auto;"
src="'+pathfield.getValue()+'"/>');
}"/>
</ImagePathField>
<previewPanel jcr:primaryType="cq:Widget" fieldLabel="Preview Pane"
height="250" html="No image to preview" xtype="panel"/>
</items>
</tab1>

How to access properties of a Tridion component like schema name based on which it is created in aspx page?

I am customizing the ribbon tool bar by adding a button to it in TRIDION 2011 SP1 version.
When I click on the button it will open an aspx page.Inside that aspx page I need to access the name of the schema used to create that component before creating the component itself(I mean to say while creating the component itself).
Please provide me a way to solve this issue. Thanks in advance.
You should pass it to your popup. The URI of the Schema is available on the Component model object within the CME - so your button command can access it and pass it to the popup (in the URL, for example).
var schemaId = $display.getView().getItem().getSchemaId();
If you have the component (as an object), you can get it's schema id as Peter indicated. If you only have the component id, you can load the component and through that get to the schema.
When you need to load any item, you have to be aware that it's not a synchronous call in the UI API, so you should use delegate methods for that. For example something like this:
Example.prototype._loadItemInformation = function Example$_loadItemInformation(itemId, reload) {
var item = $models.getItem(itemId);
if (item) {
var self = this;
function Example$_loadItemInformation$_onItemLoaded() {
$evt.removeEventHandler(item, "load", Example$_loadItemInformation$_onItemLoaded);
// proceed with the actions on the loaded item here
};
if (item.isLoaded(true) && !reload) {
Example$_loadItemInformation$_onItemLoaded();
}
else {
$evt.addEventHandler(item, "load", Example$_loadItemInformation$_onItemLoaded);
//$evt.addEventHandler(item, "loadfailed", Example$_loadItemInformation$_onItemLoadFailed);
item.load(reload, $const.OpenMode.VIEW);
}
}
};
Also be aware the item could fail loading, you should actually also register an event handler for loadfailed (as my example code neglects to do).

Rendering a spark view to a string

I'm trying to render a partial view as a string so it can be returned as HTML to a jquery ajax call. After a lot of searching I found this code.
public string RenderAsString(string viewName, string modelName, object model)
{
// Set up your spark engine goodness.
var settings = new SparkSettings().SetPageBaseType(typeof(SparkView));
var templates = new FileSystemViewFolder(Server.MapPath("~/Views"));
var engine = new SparkViewEngine(settings) { ViewFolder = templates };
// "Describe" the view (the template, it is a template after all), and its details.
var descriptor = new SparkViewDescriptor().AddTemplate(#"Shared\" + viewName + ".spark");
// Create a spark view engine instance
var view = (SparkView)engine.CreateInstance(descriptor);
// Add the model to the view data for the view to use.
view.ViewData[modelName] = model;
// Render the view to a text writer.
var writer = new StringWriter(); view.RenderView(writer);
// Convert to string
return writer.ToString();
}
But when the following line executes:
var view = (SparkView)engine.CreateInstance(descriptor);
I get the following error:
Dynamic view compilation failed. An
object reference is required for the
non-static field, method, or property
'DomainModel.Entities.Region.Id.get.
This is my partial view:
<ViewData Model="Region" />
<div id="${ Region.Id }" class="active-translation-region-widget" >
<label>${Region.RegionName}</label>
${ Html.CheckBox("Active") }
</div>
It doesn't seem to recognise the model.
P.S. When I call the view from a parent view like so
<for each="var region in Model">
<ActiveTranslationRegion Region="region" if="region.Active==true"></ActiveTranslationRegion>
</for>
It renders perfectly. What am I doing wrong?
Just from looking at it, I think the following line is the problem:
<ViewData Model="Region" />
Instead it should read:
<viewata model="Region" />
Note the lower case "model". This is because model is a special case since behind the scenes it performs a cast to a strongly typed viewmodel. The top one will define a variable called Model in the generated view class and assign the value Region to it. Using the lowercase option below will actually create a Model variable, but also cast it to strongly typed instance of Region that comes from the ViewData dictionary.
Note When using Model in the code though, like you did in the for each loop, it needs to be upper case which is correct in your case. Once again, this is the only special case because it's pulling a strongly typed model from the ViewData dictionary.
One other thing - <viewata model="Region" /> must be declared in the parent view, and it can only be defined once per page, so you cannot redefine it in a partial view. If it's a partial view, you should rather use it by passing in a part of the model like you have done in your second example above.
The reason for your exception above is because it's trying to get the Id property as a static item off the Region Type, rather than querying the Id property on your instance of Region as part of your viewmodel.
As a side note, the code to get where you want is a little mangled. You can find neater ways of doing what you want by checking out some of the Direct Usage Samples, but I understand this was probably just a spike to see it working... :)
Update in response to your follow up question/answer
I'm fairly sure that the problem is with passing the Region into the following call:
<ActiveTranslationRegion Region="region" if="region.Active==true">
... is again down to naming. Yes, you can only have one model per view as I said before, so what you need to do is remove the following from the top of your partial:
<viewdata model="Region" />
That's what's causing an issue. I would then rename the item going into your partial like so:
<ActiveTranslationRegion ActiveRegion="region" if="region.Active==true">
and then your partial would look like this:
<form action="/Translation/DeactivateRegion" class="ui-widget-content active-translation-region-widget">
<input type="hidden" name="Id" value="${ActiveRegion.Id}" />
<label class="region-name">${ ActiveRegion.RegionName }</label>
<input class="deactivate-region-button" type="image" src=${Url.Content("~/Content/Images/Deactivate.png")} alt="Deactivate" />
</form>
Note I'm using ActiveRegion because in the Spark parser, ActiveRegion gets declared as a variable and assigned the value of region in the current scope as you loop through the for loop. No need to stick religiously to the model - because you've gone and passed in a piece of the model now that you've declared as ActiveRegion. Oh, and you could stick with the name Region if you really want, I just changed it to make a point, and because you've got a Type called Region in your code and I'm not a big fan of the quirky issues using the same name for a variable as a type can bring about. Plus it makes it a little clearer.
The disadvantage of calling the Html.RenderPartial method is not immediately obvious. One thing you lose is the 3-pass rendering that Spark provides. If you use the tag syntax (which is preferable) you'll be able to stack partials within partials to multiple levels down passing variables that feed each partial what they need down the stack. It gets really powerful - start thinking data grid type structures where rows and cells are individual partials that are fed the variables they need from the model, all kept nice and clean in separate manageable view files. Don't stop there though, start thinking about targeting header and footer content base on variables or three column layouts that create a dashboard that renders all sorts on individually stacked partials many levels deep.
You lose all of that flexibility when you use the bog standard ASP.NET MVC Helper method Html.RenderPartial() - careful of doing that, there's more than likely a solution like the one above.
Let me know if that works...
All the best
Rob G
I refactored the code and views quite a bit. In the end all I'm really trying to acheive is have a parent view (not shown) iterate over an IEnumerable and for each iteration render a partial view (ActiveTranslationRegion) which renders some Html to represent a region model.
I also want to be a able to call an action method via an ajax call to render an indivual ActiveTranslationRegion partial view by passing it an individual region model. I've refactored the code accordingly.
Partial view (_ActiveTranslationRegion.spark)
<viewdata model="Region" />
<form action="/Translation/DeactivateRegion" class="ui-widget-content active-translation-region-widget">
<input type="hidden" name="Id" value="${Model.Id}" />
<label class="region-name">${ Model.RegionName }</label>
<input class="deactivate-region-button" type="image" src=${Url.Content("~/Content/Images/Deactivate.png")} alt="Deactivate" />
</form>
Notice by using I can refer to Model within the view as RobertTheGrey suggested (see above) .
I removed all the code to return the view as a string and simply created an action method method that returned a partialViewResult:
[UnitOfWork]
public PartialViewResult ActivateRegion(int id)
{
var region = _repos.Get(id);
if (region != null)
{
region.Active = true;
_repos.SaveOrUpdate(region);
}
return PartialView("_ActiveTranslationRegion", region);
}
One thing I had to do was amend my parent view to look like so:
<div id="inactive-translation-regions-panel">
<h3 class="ui-widget-header">Inactive Regions</h3>
<for each="var region in Model">
<span if="region.Active==false">
# Html.RenderPartial("_InActiveTranslationRegion", region);
</span>
</for>
</div>
Where previously I had the following:
<div id="inactive-translation-regions-panel">
<for each="var region in Model">
<ActiveTranslationRegion Region="region" if="region.Active==true"></ActiveTranslationRegion>
</for>
</div>
Notice I have to call the Html.RenderPartial rather than use the element. If I try and use the element (which I would prefer to do) I get the following error:
Only one viewdata model can be declared. IEnumerable<Region> != Region
Is there a way round that problem?
Update:
I tried your recommendation but with no luck. To recap the problem, I want to use the partial in 2 different situations. In the first instance I have a parent view that uses a model of IEnumerable<Region>, the partial simply uses Region as its model. So in the parent I iterate over the IEnumerable and pass Region to the partial. In the second instance I want to call PartialView("_ActiveTranslationRegion", region) from an action method. If I remove the <viewdata model="Region" /> from the partial I get an error complaining about the model. The best way round the problem I have found is to add a binding to the bindings.xml file:
<element name="partial"># Html.RenderPartial("#name", #model);</element>
(Note: It seems very important to keep this entry in the bindings file all on te same line)
This way I can still call the partial from the action method as described above and pass it a Region as the model, but I can also replace my call to Html.RenderPartial in the parent view with a more 'html' like tag:
<partial name="_ActiveTranslationRegion" model="region" />
So my parent view now looks more like this:
<div id="inactive-translation-regions-panel">
<h3 class="ui-widget-header">Inactive Regions</h3>
<for each="var region in Model">
<span if="region.Active==false">
<partial name="_ActiveTranslationRegion" model="region" />
</span>
</for>
</div>
Of course under the hood its still making a call to
# Html.RenderPartial("_ActiveTranslationRegion", region);
But its the best solution we could come up with.
Regards,
Simon

Resources