How to add "target" attribute to `a` tag in ckeditor5? - ckeditor5

I have create my own plugin for link. Now I want to add some other attributes to a tag generated by the plugin, like target, rel.
But I am not able to get it done. Here is the my plugins code for converter.
What converters I should add so that a tag can support other attributes?
/**
* #license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* #module link/linkediting
*/
import LinkEditing from '#ckeditor/ckeditor5-link/src/linkediting';
import {
downcastAttributeToElement
} from '#ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import { upcastElementToAttribute } from '#ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
import LinkCommand from './uclinkcommand';
import UnlinkCommand from './ucunlinkcommand';
import { createLinkElement } from '#ckeditor/ckeditor5-link/src/utils';
import { ensureSafeUrl } from './utils';
import bindTwoStepCaretToAttribute from '#ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute';
/**
* The link engine feature.
*
* It introduces the `linkHref="url"` attribute in the model which renders to the view as a `<a href="url">` element.
*
* #extends module:core/plugin~Plugin
*/
export default class UcLinkEditing extends LinkEditing {
/**
* #inheritDoc
*/
init() {
const editor = this.editor;
// Allow link attribute on all inline nodes.
editor.model.schema.extend( '$text', { allowAttributes: 'linkHref' } );
editor.conversion.for( 'dataDowncast' )
.add( downcastAttributeToElement( { model: 'linkHref', view: createLinkElement } ) );
editor.conversion.for( 'editingDowncast' )
.add( downcastAttributeToElement( { model: 'linkHref', view: ( href, writer ) => {
return createLinkElement( ensureSafeUrl( href ), writer );
} } ) );
editor.conversion.for( 'upcast' )
.add( upcastElementToAttribute( {
view: {
name: 'a',
attribute: {
href: true
}
},
model: {
key: 'linkHref',
value: viewElement => viewElement.getAttribute( 'href' )
}
} ) );
// Create linking commands.
editor.commands.add( 'ucLink', new LinkCommand( editor ) );
editor.commands.add( 'ucUnlink', new UnlinkCommand( editor ) );
// Enable two-step caret movement for `linkHref` attribute.
bindTwoStepCaretToAttribute( editor.editing.view, editor.model, this, 'linkHref' );
// Setup highlight over selected link.
this._setupLinkHighlight();
}
}

Introduction
Before I get to the code I'd like to take the occasion to explain the CKEditor 5 approach to inline elements (like <a>) so that the solution is easier to understand. With that knowledge, similar problems in the future should not be troubling. Following is meant to be a comprehensive tutorial, so expect a long read.
Even though you may know most of the things in the theory part, I recommend reading it to get the full understanding of how things work in CKEditor 5.
Also, do note that I will present a solution for the original CKEditor 5 plugin as it will be more valuable to other community members seeking a tutorial on this matter. Still, I hope that with the insight from this tutorial you will be able to adjust the code sample to your custom plugin.
Also, keep in mind that this tutorial does not discuss UI part of this plugin, only how things should be configured for conversion purposes. Adding and removing attributes is the job for UI or for some other part of code. Here I discuss only engine stuff.
Inline elements in CKEditor 5
First, let's establish which elements are inline. By inline elements I understand elements like <strong>, <a> or <span>. Unlike <p>, <blockquote> or <div>, inline elements do not structure the data. Instead, they mark some text in a specific (visual and semantical) way. So, in a way, these elements are a characteristic of a given part of a text. As a result, we say that given part of a text is bold, or that given part of a text is/has a link.
Similarly, in the model, we don't represent <a> or <strong> directly as elements. Instead, we allow adding attributes to a part of a text. This is how text characteristics (as bold, italic or link) are represented.
For example, in the model, we might have a <paragraph> element with Foo bar text, where bar has the bold attribute set to true. We would note it this way: <paragraph>Foo <$text bold="true">bar</$text></paragraph>. See, that there is no <strong> or any other additional element there. It's just some text with an attribute. Later, the bold attribute is converted to <strong> element.
By the way: view elements that come from model attributes have their own class: view.AttributeElement and instead of inline elements can be also called attribute elements. Sadly, the name conflicts with "attribute" as an attribute of a view element (what is worse, attribute element is allowed to have attributes).
Of course, text may have multiple attributes and all of them are converted to their respective view inline elements. Keep in mind that in the model, attributes do not have any set order. This is contrary to the view or HTML, where inline elements are nested one in another. The nesting happens during conversion from the model to the view. This makes working in model simpler, as features do not need to take care of breaking or rearranging elements in the model.
Consider this model string:
<paragraph>
<$text bold="true">Foo </$text>
<$text bold="true" linkHref="bar.html">bar</$text>
<$text bold="true"> baz</$text>
</paragraph>
It is a bold Foo bar baz text with a link on bar. During conversion, it will be converted to:
<p>
<strong>Foo </strong><strong>bar</strong><strong> baz</strong>
</p>
Note, that the <a> element is converted in a way that it is always the topmost element. This is intentional so that none element will ever break an <a> element. See this, incorrect view/HTML string:
<p>
Foo <strong>bar</strong>
</p>
The generated view/HTML has two link elements next to each other, which is wrong.
We use priority property of view.AttributeElement to define which element should be on top of others. Most elements, like <strong> do not care about it and keep the default priority. However, <a> element has changed priority to guarantee a proper order in the view/HTML.
Complex inline elements and merging
So far we mostly discussed the simpler inline elements, i.e. elements which don't have attributes. Examples are <strong>, <em>. In contrary, <a> has additional attributes.
It is easy to come up with features that need to mark/style a part of a text but are custom enough so that simply using a tag is not enough. An example would be a font family feature. When used, it adds fontFamily attribute to a text, which is later converted to <span> element with an appropriate style attribute.
At this point, you need to ask what should happen if multiple such attributes are set on the same part of a text? Take this model example:
<paragraph>
<$text fontFamily="Tahoma" fontSize="big">Foo</$text>
</paragraph>
The above attributes convert as follow:
fontFamily="value" converts to <span style="font-family: value;">,
fontSize="value" converts to <span class="text-value">.
So, what kind of view/HTML could we expect?
<p>
<span style="font-family: Tahoma;">
<span class="text-big">Foo</span>
</span>
</p>
This, however, seems wrong. Why not have just one <span> element? Wouldn't it be better this way?
<p>
<span style="font-family: Tahoma;" class="text-big">Foo</span>
</p>
To solve situations like these, in CKEditor 5 conversion mechanism we, in fact, introduced a merging mechanism.
In the above scenario, we have two attributes that convert to <span>. When the first attribute (say, fontFamily is converted, there is no <span> in the view yet. So the <span> is added with the style attribute. However, when fontSize is converted, there is already <span> in the view. view.Writer recognizes this and checks whether those elements can be merged. The rules are three:
elements must have the same view.Element#name,
elements must have the same view.AttributeElement#priority,
neither element may have view.AttributeElement#id set.
We haven't discussed id property yet but, for simplicity reasons, I won't talk about it now. It is enough to say that it is important for some attribute elements to prevent merging them.
Adding another attribute to the link
At this point, it should be pretty clear how to add another attribute to <a> element.
All that needs to be done is defining a new model attribute (linkTarget or linkRel) and make it convert to <a> element with the desired (target="..." or rel="...") attribute. Then, it will be merged with the original <a href="..."> element.
Keep in mind that <a> element from the original CKEditor 5 link plugin has custom priority specified. This means that the element generated by the new plugin need to have the same priority specified to be properly merged.
Upcasting merged attribute elements
For now, we only discussed downcasting (i.e. converting from the model to the view). Now let's talk about upcasting (i.e. converting from the view to the model). Fortunately, it is easier than the previous part.
There are two "things" that can be upcasted - elements and attributes. No magic here - elements are elements (<p>, <a>, <strong>, etc.) and attributes are attributes (class="", href="", etc.).
Elements can be upcast to elements (<p> -> <paragraph>) or attributes (<strong> -> bold, <a> -> linkHref). Attributes can be upcast to attributes.
Our example clearly needs upcasting from an element to an attribute. Indeed, <a> element is converted to linkHref attribute and the linkHref attribute value is taken from href="" attribute of the <a> element.
Naturally, one would define the same conversion for their new linkTarget or linkRel attribute. However, there is a trap here. Each part of the view can be converted ("consumed") only once (this is also true for the model when downcasting).
What does it mean? Simply, if one feature already converted given element name or given element attribute, neither feature can also convert it. This way features can correctly overwrite each other. This also means that general-purpose converters can be introduced (for example, <div> can be converted to <paragraph> if no other feature recognized <div> as something that can be converted by that feature). This also helps to spot conflicting converters.
Back to our example. We cannot define two element-to-attribute converters that convert the same element (<a>) and expect them to work together at the same time. One will overwrite the other.
Since we don't want to change the original link plugin, we need to keep that converter as is. However, the upcast converter for the new plugin will be an attribute-to-attribute converter. Since that converter won't convert element (or rather, element name) it will work together with the original converter.
Code sample
Here is a code sample for a link target plugin. Below I will explain some parts of it.
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
import { downcastAttributeToElement } from '#ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import { upcastAttributeToAttribute } from '#ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
class LinkTarget extends Plugin {
init() {
const editor = this.editor;
editor.model.schema.extend( '$text', { allowAttributes: 'linkTarget' } );
editor.conversion.for( 'downcast' ).add( downcastAttributeToElement( {
model: 'linkTarget',
view: ( attributeValue, writer ) => {
return writer.createAttributeElement( 'a', { target: attributeValue }, { priority: 5 } );
},
converterPriority: 'low'
} ) );
editor.conversion.for( 'upcast' ).add( upcastAttributeToAttribute( {
view: {
name: 'a',
key: 'target'
},
model: 'linkTarget',
converterPriority: 'low'
} ) );
}
}
For such a long tutorial it surely is a small snippet. Hopefully, most of it is self-explanatory.
First, we expand Schema by defining a new attribute linkTarget that is allowed on text.
Then, we define downcast conversion. downcastAttributeToElement is used as we want to create <a target="..."> element that will be merged with the original <a> element. Keep in mind that the <a> element that is created here has the priority defined to 5, just as in the original link plugin.
The last step is upcast conversion. upcastAttributeToAttribute helper is used, as discussed earlier. In view configuration, it is specified that only target attribute of <a> element should be converted (name: 'a'). This does not mean that <a> element will be converted! This is only filtering configuration for the converter, so it won't convert target attribute of some other element.
Lastly, both converters are added with priority lower than the original converters to prevent any hypothetical problems.
The above sample works for me on the current master of ckeditor5-engine and ckeditor5-link.

Szymon Cofalik's answer is great, but not working anymore for at least the current CKE5 version (34.2.0).
The functions downcastAttributeToElement() and upcastAttributeToAttribute() are not exported anymore, so you have to use attributeToElement() and attributeToAttribute() from the Conversion API, which are available by default.
Updated code example:
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
class LinkTarget extends Plugin {
init() {
const editor = this.editor;
editor.model.schema.extend( '$text', { allowAttributes: 'linkTarget' } );
editor.conversion.for( 'downcast' ).attributeToElement( {
model: 'linkTarget',
view: ( attributeValue, writer ) => {
return writer.createAttributeElement( 'a', { target: attributeValue }, { priority: 5 } );
},
converterPriority: 'low'
} );
editor.conversion.for( 'upcast' ).attributeToAttribute( {
view: {
name: 'a',
key: 'target'
},
model: 'linkTarget',
converterPriority: 'low'
} );
}
}

as I came to the same problem in 2022, I founded this Answer very helpful, I wanted to add id attribute but didn't create my own plugin, I just edited the Link plugin in ckeditor5-build-classic package then I re-builded it.
1- in #module link/linkediting:
Allow link attribute on all inline nodes.
editor.model.schema.extend( '$text', { allowAttributes: ['linkHref', 'linkId'] } );
add conversion for upcast to conserve existed id attribute or create new one:
editor.conversion.for( 'upcast' ).attributeToAttribute( {
view: {
name: 'a'
},
model: {
key: 'linkId',
value: viewElement => {
let id = viewElement.getAttribute( 'id' );
if (id)
return id;
return 'id_'+Math.floor(Math.random() * 10000)
}
},
converterPriority: 'low'
} ) ;
add conversion for editingDowncast, to transform the Model into the view:
editor.conversion.for( 'editingDowncast' ).attributeToElement( {
model: 'linkId',
view: ( attributeValue, conversionApi ) => {
return conversionApi.writer.createAttributeElement( 'a', { id: attributeValue }, { priority: 5 } );
},
converterPriority: 'low'
} ) ;
add conversion for dataDowncast, to get the Attribute when getDate() is called:
editor.conversion.for( 'dataDowncast' )
.attributeToElement( {
model: 'linkId',
view: ( attributeValue, conversionApi ) => {
return conversionApi.writer.createAttributeElement( 'a', { id: attributeValue }, { priority: 5 } );
}
} ) ;
2- in #module link/linkcommand : to create the id, I just wanted a random string so I didn't made any new field in the form, and just added this line after the one that is responsible for linkHref Attribute
writer.setAttribute( 'linkId', 'id_'+Math.floor(Math.random() * 10000), range );

Related

Spartacus Configurable Product Integration: Custom Normalizer nested data being overridden

I am trying to implement a custom normalizer to the configurable product feature module. I have to include a custom field in the Attributes datatype. Currently only the OccConfigurationVariantNormalizer is available, which is quite high level form a data's point of view.
My problem occurs with the execution order of the normalizers. The default normalizer ist this: https://github.com/SAP/spartacus/blob/develop/feature-libs/product-configurator/rulebased/occ/variant/converters/occ-configurator-variant-normalizer.ts which is being called after my custom normalizer. Hence, the convertGroup() function is overriding my custom attribute field.
Here is my implementation:
#Injectable({
providedIn: 'root'
})
export class CustomConfiguratorNormalizerService extends OccConfiguratorVariantNormalizer{
convertAttribute(sourceAttribute: CustomOccAttribute, attributeList: CustomAttribute[]): void{
super.convertAttribute(sourceAttribute, attributeList);
attributeList[attributeList.length - 1].customField = sourceAttribute.customField;
}
}
Extending the original Normalizer seemed like the most promising solution for the time being, and is working quite like intended. So the customField ist being present at this point in time of execution.
Afterwards the OccConfiguratorVariantNormalizer kicks in, which is defining a new Attribute array in convertGroup(), erasing my custom attribute:
convertGroup([...]) {
const attributes: Configurator.Attribute[] = [];
if (source.attributes) {
source.attributes.forEach((sourceAttribute) =>
this.convertAttribute(sourceAttribute, attributes)
);
}
[...]
};
convertAttribute(
sourceAttribute: OccConfigurator.Attribute,
attributeList: Configurator.Attribute[]
): void {
const attribute: Configurator.Attribute = {
name: sourceAttribute.name,
label: sourceAttribute.langDepName,
required: sourceAttribute.required,
uiType: this.convertAttributeType(sourceAttribute.type),
values: [],
groupId: this.getGroupId(sourceAttribute.key, sourceAttribute.name),
userInput: sourceAttribute.formattedValue,
maxlength:
sourceAttribute.maxlength + (sourceAttribute.negativeAllowed ? 1 : 0),
numDecimalPlaces: sourceAttribute.numberScale,
negativeAllowed: sourceAttribute.negativeAllowed,
numTotalLength: sourceAttribute.typeLength,
selectedSingleValue: null,
images: [],
hasConflicts: sourceAttribute?.conflicts?.length > 0 ? true : false,
};
[...]
};
If my custom normalizer was the only one I could imagine it would work, which is why I tried to inject it like this:
{
provide: VARIANT_CONFIGURATOR_NORMALIZER,
useClass: CustomConfiguratorNormalizerService,
multi: false,
}
Throwing me Error: Multi-providers mixed with single providers.
Also, using the documentation from https://sap.github.io/spartacus-docs/connecting-to-other-systems/ I cannot get it to work without extending the original Normalizer, since target will always be undefined, which probably would not be the case if my custom normalizer came in second.
I feel like this https://github.com/SAP/spartacus/issues/9046 could be related.
Any help very much appreciated :)
I was able to solve this myself. Following the reference structure for spartacus applications at https://sap.github.io/spartacus-docs/reference-app-structure/ the problem disappeared.
My best guess is that it has to do with the import order of the modules. In my current working version I import the FeaturesModule last, which seems to solve the problem.

Event handling after HTML injection with Vue.js

Vue is not registering event handler for HTML injected objects. How do I do this manually or what is a better way to work around my problem?
Specifically, I send a query to my server to find a token in text and return the context (surrounding text) of that token as it exists in unstructured natural language. The server also goes through the context and finds a list of those words that also happen to be in my token set.
When I render to my page I want all of these found tokens in the list to be clickable so that I can send the text of that token as a new search query. The big problem I am having is my issue does not conform to a template. The clickable text varies in number and positioning.
An example of what I am talking about is that my return may look like:
{
"context": "When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected",
"chunks": ['human events', 'one people', 'political bands']
}
And the resulting output I am looking for is the sentence looks something like this in psuedocode:
When in the Course of <a #click='search("human events")'>human events</a>, it becomes necessary for <a #click='search("one people")'>one people</a> to dissolve the <a #click='search("political bands")'>political bands</a> which have connected
This is what I have tried so far though the click handler is not registered and the function never gets called:
<v-flex xs10 v-html="addlink(context.context, context.chunks)"></v-flex>
and in my methods section:
addlink: function(words, matchterms){
for(var index in matchterms){
var regquery = matchterms[index].replace(this.regEscape, '\\$&');
var query = matchterms[index];
var regEx = new RegExp(regquery, "ig");
words = words.replace(regEx, '<a href=\'#\' v-on:click.prevent=\'doSearch("'+ query +'")\'>' + query + '</a>');
}
return words;
}
As I said, this does not work and I know why. This is just showing that because of the nature of the problem is seems like regex is the correct solution but that gets me into a v-html injection situation. Is there something I can do in Vue to register the event handlers or can some one tell me a better way to load this data so I keep my links inline with the sentence and make them functional as well?
I've already posted one answer but I've just realised that there's a totally different approach that might work depending on your circumstances.
You could use event delegation. So rather than putting click listeners on each <a> you could put a single listener on the wrapper element. Within the listener you could then check whether the clicked element was an <a> (using event.target) and act accordingly.
Here's one way you could approach it:
<template>
<div>
<template v-for="segment in textSegments">
<a v-if="segment.link" href="#" #click.prevent="search(segment.text)">
{{ segment.text }}
</a>
<template v-else>
{{ segment.text }}
</template>
</template>
</div>
</template>
<script>
export default {
data () {
return {
"context": "When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected",
"chunks": ['human events', 'one people', 'political bands']
}
},
computed: {
textSegments () {
const chunks = this.chunks
// This needs escaping correctly
const re = new RegExp('(' + chunks.join('|') + ')', 'gi')
// The filter removes empty strings
const segments = this.context.split(re).filter(text => text)
return segments.map(segment => {
return {
link: segment.match(re),
text: segment
}
})
}
},
methods: {
search (chunk) {
console.log(chunk)
}
}
}
</script>
I've parsed the context text into an array of segments that can then be handled cleanly using Vue's template syntax.
I've used a single RegExp and split, which will not discard matches if you wrap them in a capture group, (...).
Going back to your original example, v-html only supports native HTML, not Vue template syntax. So you can add events using onclick attributes but not #click or v-on:click. However, using onclick wouldn't provide easy access to your search method, which is scoped to your component.

Complex object in a dropdown using JSViews

I am working on project with JSViews, Observables and TypeScript.
I planned to handle several languages so I have an object that holds french and english version. A class with static methods that returns the collection of names and an array with all the static objects.
I wanted to display the objects in a dropdown with a converter to fetch the english name, I managed to fill the dropdown and react on change but I can't display the current item in the dropdown and I don't see what is missing.
Could you please help ? I made a javascript sample here :
https://jsfiddle.net/ClaudeVernier/v093uqg0/
var data = new dataModel();
for (var member in Harbors) {
if (typeof Harbors[member] == "object" && Harbors[member].name) {
data.harbors.push(Harbors[member]);
}
}
var myTmpl = $.templates("#harborTmpl");
myTmpl.link("#container", data);
$.observe(data, "*", myHandler);
Then, I'll need to figure how to change language on the click of a button, if you have idea on that... it would help :-)
Many thanks,
Claude
Take a look at Data-linked <select> elements - including the section <select> with converters.
Your code:
<select id="cboHarbor" data-link="{{getName:activeHarbor}}">
is incorrect. You need to data-link to activeHarbor. If activeHarbor was a string, you could data-link using:
<select id="cboHarbor" data-link="activeHarbor">
but since it is an object you need to have some kind of string-valued property for each harbor object, that you can then use for each option value. Then you will use converters to convert back and forth between the activeHarbor object and its string value property for the data-link binding behavior.
For example you can use the name in the current language as string value, but it is a bit strange to use a value that changes based on current language. But you need a getHarbor converter to convert back to get from the name to the harbor object. That would look like this:
<select id="cboHarbor" data-link="{getName:activeHarbor:getHarbor}">
{^{for harbors}}
<option data-link="{getName:}"></option>
{{/for}}
</select>
Alternatively you can use the index of the harbor in the array, like this:
<select id="cboHarbor" data-link="{getIndex:activeHarbor:getHarbor}">
{^{for harbors}}
<option value="{{:#index}}" data-link="{getName:}"></option>
{{/for}}
</select>
with converters as follows:
$.views.converters({
getIndex: function (harbor) {
return harbor.index;
},
getHarbor: function (index) {
return data.harbors[index];
},
getName: function (harbor) {
return harbor.name[data.languages[data.currentLanguage]];
}
});
If you want to be able to dynamically change language and have the harbors drop-down switch to the new language, then you must make your getName converter depend on the current language, like this:
$.views.converters.getName.depends = [data, "currentLanguage"];
Here is an updated version of your jsfiddle complete with a language drop-down to switch language.
UPDATE:
Concerning the depends for getName, a modified jsFiddle has this:
function getName(harbor) {
return harbor.name[data.languages[data.currentLanguage]];
}
$.views.converters({
getName: getName,
...
});
getName.depends = [data, "currentLanguage"];
So you can simply use a getName function as your converter function, and then in the context in which you have access to the data instance (in a done() if it needs to be async), you then assign the depends:
getName.depends = [data, "currentLanguage"];
No need to use $.views.converters.getName.depends

Display content inline with a dijit Tree

I'm using Dojo and would like to create a tree like structure. However, I'd like to be able to display content within the tree once the end node in a particular branch has been expanded. e.g.
top
- a branch
-- last item in this branch
[some content such as a div, span, image etc]
-- another item in this branch
[some more content]
etc
Does anyone know if this can be achieved using dijit Tree and if so, any pointers?
After digging around in the docs I've found a way to do this. It's not simple so thought I'd share. This page has an example of how to display a tree node with a rich text label rather than just text. This involves declaring your own class that inherits from Tree._TreeNode, allowing you to control it's creation. This same technique can be used.
When creating a Tree, I override the _createTreeNode function as follows:
_createTreeNode: function (args) {
if (args.item.type === "content") {
return new LayerManagerContentNode(args);
} else {
return new Tree._TreeNode(args);
}
}
In my store I add an object to represent the content that I want to display inline and give it a type of 'content'.
I create a class that inherits from Tree._TreeNode as follows:
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dojo/dom",
"dojo/dom-construct",
"dojo/on",
"dijit/form/Button",
"dijit/Tree"
], function (declare, lang, dom, domConstruct, on, Button, Tree) {
return declare("my/ui/platforms/desktop/parts/LayerManagerContentNode", [Tree._TreeNode], {
// summary:
// ...
constructor: function () {
},
postCreate: function () {
var button = new Button({
label: "Test"
});
this.domNode.innerHTML = "<div></div>";
this.domNode.innterText = "";
button.placeAt(this.domNode, "first");
}
});
});
in the postCreate, I create a button (this was just for testing, I'll probable create a content pane or something to further populate) to be displayed in place of the usual tree node. I then replace the tree nodes innerHTML and innerText to hide what would normally be displayed, en voila, it works!
I dare say there are better ways to do this so if anyone comes along and has one, please add it.

How to set custom DOM ID for tree nodes in Dojo 1.4?

I have written an application using Dojo 1.3 in which I have used the Dijit Tree component.
I am loading the tree with JSON data specified in the store which is in turn used by the tree model.
In the 1.3 version the tree elements pick up the id directly from the JSON data. However in 1.4 the tree elements have their own id which is something like dijit_treenode_4. The id's that I have specified in JSON are unique and im not able to understand why these are not being used anymore.
Please help me understand how this functionality has changed and how I can override the automatic id generation.
Thanks in advance,
Fell
I think this design decision is a nice improvement on separating the model and view. The identifiers in store data really shouldn't have any impact on the tree node's DOM element ID by default.
Of course, if you want this behavior, you can achieve it. Define your own tree node class that overrides the DOM element ID.
dojo.require('dijit.Tree');
dojo.declare('yourapp.TreeNode', [dijit._TreeNode], {
// summary:
// Tree node with custom ID.
postCreate: function(){
// summary:
// Overrides the normal `postCreate()` method to set custom ID
// for node DOM element.
this.inherited(arguments);
var id = this.tree.model.store.getValue(this.item, 'id');
this.domNode.id = id;
}
});
Then subclass dijit.Tree, and override method _createTreeNode() to create your custom tree nodes.
dojo.declare('yourapp.Tree', [dijit.Tree], {
_createTreeNode: function(args) {
return new yourapp.TreeNode(args);
}
});