Dojo _TemplatedMixin: changing templateString - dojo

How do I change the template of a widget, using mixins dijit/_TemplatedMixin and dijit/_WidgetsInTemplateMixin, at a later time (not in the constructor)?
My scenario is that the widget must make a call to the server to get data, and the callback function will then merge the data with a template file and then the resulting template should be used for the templateString. The widget should update its contents at this point.
Setting the templateString and calling buildRendering() has no effect.
Here is a simplified version of my code:
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_WidgetBase",
"dijit/_TemplatedMixin",
"dijit/_WidgetsInTemplateMixin",
],
function(declare, lang, _WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin) {
return declare([_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin], {
constructor: function(id) {
this.id = id;
this.templateString = "<div>Loading...</div>";
//use xhr to call REST service to get data.
//dataLoadedCallback is executed with response data
...
},
dataLoadedCallback : function(data) {
this.destroyRendering();
//render a templateString using the data response from the rest call
this.templateString = "<div>Data is loaded. Name:" + data.name + "</div>"
this.buildRendering();
},
});
});

You cannot do such thing. The template is parsed only once before postCreate method.
However there is few things you can do:
Create a non-ui widget which will do the XHR call. When this non-ui widget get the XHR response it creates the UI widget with the correct templateString
Or use dojo/dom-construct. It contains a toDom method which you can use for converting your string into nodes. Then you can append that to the widget.
Note: this will not parse any data-dojo attributes
You could also directly inject the received templateString into the widget domNode:
dataLoadedCallback : function(data) {
this.domNode.innerHTML = "<div>Data is loaded. Name:" + data.name + "</div>";
//you might be able to parse the content (if you have subwidgets) using dojo/parse
},
Last but not least, here is a util I wrote for my self. It allow to parse any templateString at any time (like dojo does on widget creation)
define([
'dojo/dom-construct',
'dojo/string',
'dijit/_AttachMixin',
'dijit/_TemplatedMixin'
], function(domConstruct, string,
_AttachMixin, _TemplatedMixin) {
// summary:
// provide an utility to parse a template a runtime (and create attach point, atach events, etc...)
// Copyright: Benjamin Santalucia
var GET_ATTRIBUTE_FUNCTION = function(n, p) { return n.getAttribute(p); },
_TemplateParserMixin = function() {};
_TemplateParserMixin.prototype = {
parseTemplate: function(template, data, container, position, transformer) {
// summary:
// parse the template exactly as dojo will nativly do with a templateString
if(this._attachPoints === undefined) {
this._attachPoints = [];
}
if(this._attachEvents === undefined) {
this._attachEvents = [];
}
var nodes,
x,
len,
newTemplate = string.substitute(template, data, transformer),
node = domConstruct.toDom(_TemplatedMixin.prototype._stringRepl.call(this, newTemplate));
if(node.nodeName === '#document-fragment') {
node = node.firstChild;
}
//parse all nodes and create attach points and attach events
nodes = node.getElementsByTagName('*');
len = nodes.length;
for(x = -1; x < len; x++) {
_AttachMixin.prototype._processTemplateNode.call(this, x < 0 ? node : nodes[x], GET_ATTRIBUTE_FUNCTION, _AttachMixin.prototype._attach);
}
if(container) {
domConstruct.place(node, container, position);
}
return node;
}
};
return _TemplateParserMixin;
});
Usage is:
returnedNode = w.parseTemplate(newTemplateString, {
templatePlaceHolderName: 'foo' //for teplate with placeholders like ${templatePlaceHolderName}
}, domNodeToInsertIn, 'only'); //last parameter is same as dojo/dom-construct::place() >> last, first, before, after, only

Related

Manipulate innerText of a CKEditor ViewElement

I am creating a little custom plugin for the CKEditor5 for the #neoscms.
Neos is using the #ckeditor5 but with a custom view.
The plugin is more or less a placeholder plugin. The user can configure a data-source with a key value store for items (identifier and labels). The dropdown in the CKEditor is filled with the items and when the user selects an item from the dropdown, it creates a placeholder element that should end in a span element with some data-attributes.
The main idea was to have an empty element and just data-attributes to identify the element and being able to assign live data. But it turns out that the live data thing is tricky. When I manipulate the span with an extra JS snippet on the Website, the CKEditor cannot handle this.
Is it possible to manipulate a view element in the DOM and still have a working Editor?
The Plugin works fine if I just add inner Text in the downCasting and don't replace something. But the live data would be nice.
Neos Backend with a element
Maybe that code gives an idea of the package.
It is not ready yet as this is more or less the main feature ;)
import {Plugin, toWidget, viewToModelPositionOutsideModelElement, Widget,} from "ckeditor5-exports";
import PlaceholderCommand from "./placeHolderCommand";
export default class PlaceholderEditing extends Plugin {
static get requires() {
return [Widget];
}
init() {
this._defineSchema();
this._defineConverters();
this.editor.commands.add(
"placeholder",
new PlaceholderCommand(this.editor)
);
this.editor.editing.mapper.on(
"viewToModelPosition",
viewToModelPositionOutsideModelElement(this.editor.model, (viewElement) =>
viewElement.hasClass("internezzo-placeholder")
)
);
this.editor.config.define("placeholderProps", {
types: ["name", "node", "nodePath"],
});
this.editor.config.define("placeholderBrackets", {
open: "[",
close: "]",
});
}
_defineSchema() {
const schema = this.editor.model.schema;
schema.register("placeholder", {
allowWhere: "$text",
isInline: true,
isObject: true,
allowAttributes: [
"name",
"node",
"nodePath",
"data-placeholder-identifier",
"data-node-identifier",
"data-node-path",
],
});
}
_defineConverters() {
const conversion = this.editor.conversion;
const config = this.editor.config;
conversion.for("upcast").elementToElement({
view: {
name: "span",
classes: ["foobar-placeholder"],
},
model: (viewElement, writer) => {
const name = viewElement.getAttribute('data-placeholder-identifier');
const node = viewElement.getAttribute('data-node-identifier');
const nodePath = viewElement.getAttribute('data-node-path');
const modelWriter = writer.writer || writer;
return modelWriter.createElement("placeholder", {name, node, nodePath, editable: false});
},
});
conversion.for("editingDowncast").elementToElement({
model: "placeholder",
view: (modelItem, writer) => {
const viewWriter = writer.writer || writer;
const widgetElement = createPlaceholderView(modelItem, viewWriter);
return toWidget(widgetElement, viewWriter);
},
});
conversion.for("dataDowncast").elementToElement({
model: "placeholder",
view: (modelItem, writer) => {
const viewWriter = writer.writer || writer;
return createPlaceholderView(modelItem, viewWriter);
},
});
// Helper method for downcast converters.
function createPlaceholderView(modelItem, viewWriter) {
const name = modelItem.getAttribute("name");
const node = modelItem.getAttribute("node");
const nodePath = modelItem.getAttribute("nodePath");
const placeholderView = viewWriter.createContainerElement("span", {
class: "foobar-placeholder",
"data-placeholder-identifier": name,
"data-node-identifier": node,
"data-node-path": nodePath,
});
// Would be nice to remove that and have just empty spans that get dynamic data
let innerText = config.get("placeholderBrackets.open") + name;
innerText += config.get("placeholderBrackets.close");
viewWriter.insert(
viewWriter.createPositionAt(placeholderView, 0),
viewWriter.createText(innerText)
);
return placeholderView;
}
}
}
So, the extra JS snippet that is used by the website is searching for spans with the class foobar-placeholder and writes a value with live data into the span. That works in the frontend, of course, but the backend of the CMS that uses CKEditor has issues with the changing data.
I could not find a solution with docs of CKEditor, and maybe I misuse the API somehow, but I now found a working solution for me.
My website snippet is now communicating with the Plugin via Broadcast messages. And then I search for placeholder elements and check if I need to change an attribute.
const broadcastChannel = new BroadcastChannel('placeholder:changeData');
broadcastChannel.postMessage({identifier: name, value});
And in the plugin
// Receive new values for placeholder via broadcast
const broadcastChannel = new BroadcastChannel('placeholder:changeData');
broadcastChannel.onmessage = (message) => {
const identifier = get('data.identifier', message);
const newValue = get('data.value', message);
this.editor.model.change( writer => {
if (identifier) {
this._replaceAttribute(writer, identifier, newValue);
}
});
};
Only downside now is that I need to reload the page, but already read that this is maybe cause by my element down casting and I change attributes.

How we get and post api in Titanium alloy?

How can we get and post api in Titanium alloy?
I am having the api of userDetails, I just want that how can i code to get the data from api.
function getUserDetails(){
}
Thanks in advance.
As you mentioned, you are using Titanium alloy.
So another approach be to extend the Alloy's Model and Collection ( which are based on backbone.js concept ).
There are already some implementation at RestAPI Sync Adapter also proper description/usage at Titanium RestApi sync.
I also provide the description and methodology used, in-case link gets broken:
Create a Model : Alloy Models are extensions of Backbone.js Models, so when you're defining specific information about your data, you do it by implementing certain methods common to all Backbone Models, therefor overriding the parent methods. Here we will override the url() method of backbone to allow our custom url endpoint.
Path :/app/models/node.js
exports.definition = {
config: {
adapter: {
type: "rest",
collection_name: "node"
}
},
extendCollection: function(Collection) {
_.extend(Collection.prototype, {
url: function() {
return "http://www.example.com/ws/node";
},
});
return Collection;
}
};
Configure a REST sync adapter : The main purpose of a sync adapter is to override Backbone's default sync method with something that fetches your data. In our example, we'll run through a few integrity checks before calling a function to fetch our data using a Ti.Network.createHTTPClient() call. This will create an object that we can attach headers and handlers to and eventually open and send an xml http request to our server so we can then fetch the data and apply it to our collection.
Path :/app/assets/alloy/sync/rest.js (you may have to create alloy/sync folders first)
// Override the Backbone.sync method with our own sync
functionmodule.exports.sync = function (method, model, opts)
{
var methodMap = {
'create': 'POST',
'read': 'GET',
'update': 'PUT',
'delete': 'DELETE'
};
var type = methodMap[method];
var params = _.extend(
{}, opts);
params.type = type;
//set default headers
params.headers = params.headers || {};
// We need to ensure that we have a base url.
if (!params.url)
{
params.url = model.url();
if (!params.url)
{
Ti.API.error("[REST API] ERROR: NO BASE URL");
return;
}
}
//json data transfers
params.headers['Content-Type'] = 'application/json';
switch (method)
{
case 'delete':
case 'create':
case 'update':
throw "Not Implemented";
break;
case 'read':
fetchData(params, function (_response)
{
if (_response.success)
{
var data = JSON.parse(_response.responseText);
params.success(data, _response.responseText);
}
else
{
params.error(JSON.parse(_response.responseText), _response.responseText);
Ti.API.error('[REST API] ERROR: ' + _response.responseText);
}
});
break;
}
};
function fetchData(_options, _callback)
{
var xhr = Ti.Network.createHTTPClient(
{
timeout: 5000
});
//Prepare the request
xhr.open(_options.type, _options.url);
xhr.onload = function (e)
{
_callback(
{
success: true,
responseText: this.responseText || null,
responseData: this.responseData || null
});
};
//Handle error
xhr.onerror = function (e)
{
_callback(
{
'success': false,
'responseText': e.error
});
Ti.API.error('[REST API] fetchData ERROR: ' + xhr.responseText);
};
for (var header in _options.headers)
{
xhr.setRequestHeader(header, _options.headers[header]);
}
if (_options.beforeSend)
{
_options.beforeSend(xhr);
}
xhr.send(_options.data || null);
}
//we need underscore
var _ = require("alloy/underscore")._;
Setup your View for Model-view binding : Titanium has a feature called Model-View binding, which allows you to create repeatable objects in part of a view for each model in a collection. In our example we'll use a TableView element with the dataCollection property set to node, which is the name of our model, and we'll create a TableViewRow element inside. The row based element will magically repeat for every item in the collection.
Path :/app/views/index.xml
<Alloy>
<Collection src="node">
<Window class="container">
<TableView id="nodeTable" dataCollection="node">
<TableViewRow title="{title}" color="black" />
</TableView>
</Window>
</Alloy>
Finally Controller : Binding the Model to the View requires almost no code at the controller level, the only thing we have to do here is load our collection and initiate a fetch command and the data will be ready to be bound to the view.
Path :/app/controllers/index.js
$.index.open();
var node = Alloy.Collections.node;
node.fetch();
Further reading :
Alloy Models
Sync Adapters
Hope it is helpful.
this is the solution for your problem:-
var request = Titanium.Network.createHTTPClient();
var done=false;
request.onload = function() {
try {
if (this.readyState == 4 && !done) {
done=true;
if(this.status===200){
var content = JSON.parse(this.responseText);
}else{
alert('error code' + this.status);
}
}
} catch (err) {
Titanium.API.error(err);
Titanium.UI.createAlertDialog({
message : err,
title : "Remote Server Error"
});
}
};
request.onerror = function(e) {
Ti.API.info(e.error);
};
request.open("POST", "http://test.com");
request.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
request.send({ test: 'test'});
if you don't get your answer please let me know.
Thanks

Dojo instances of same widgets are not saparated

I have built a Dojo Widget for creating a list by entering values. the widget code is:
define(["dojo/_base/declare", "dijit/_WidgetBase", "dijit/_TemplatedMixin", 'dojo/text!apps/orders/templates/multiAddList.html', "dojo/dom", "dojo/on", "dojo/dom-construct", "dojo/dom-class", "dojo/query", "dijit/focus"],
function (declare, WidgetBase, TemplatedMixin, html, dom, on, domConstruct, domClass, query, focusUtil) {
return declare([WidgetBase, TemplatedMixin], {
templateString: html,
postCreate: function () {
this.inherited(arguments);
var that = this;
},
_checkIfEnter: function (e) {
if (e.which == 13) {
this._addUser();
}
},
_addUser: function () {
domClass.remove(this.ulAdded, "hidden");
var textToAdd = this.userTextToAdd.value;
var li = domConstruct.create("li", {}, this.ulAdded);
domConstruct.create("span", {innerHTML: textToAdd}, li);
var spanX = domConstruct.create("span", {class: 'icon-x right'}, li);
this.itemsArray.push(textToAdd);
this.userTextToAdd.value = "";
focusUtil.focus(this.userTextToAdd);
var that = this;
on(spanX, "click", function () {
domConstruct.destroy(li);
that.itemsArray.splice(that.itemsArray.indexOf(textToAdd), 1);
if (that.itemsArray.length == 0) {
domClass.add(that.ulAdded, "hidden");
}
});
},
itemsArray: []
});
});
It is all OK. However - when I instantiate it twice on same dialog like this:
allowedDomains = new MultiAddList();
allowedDomains.placeAt(dom.byId('allowedDomains'), 0);
pdlEmails = new MultiAddList();
pdlEmails.placeAt(dom.byId('pdlEmails'), 0);
and then asking for allowedDomains.itemsArray() or pdlEmails.itemsArray() - I get the same list (as if it is the same instance) - althought in the UI presentation - he adds the list items separately and correctly.
Obviously, I am doing something wrong although I followed Dojo examples.
Does anyone know what I should do in order to make it work?
Thanks
When you make a dojo class using declare, object and array members are static, meaning they are shared across instances, so I would suggest doing itemsArray: null and then this.itemsArray = [] in the constructor or postCreate somewhere.
Everything else looks fine, although I too would have a preference for using hitch, your solution is perfectly fine.
Sorry for just giving you a hint, but you might want to look at the dojo.hicth()-function, as an alternative to the "this-that" contruction
on(spanX, "click", dojo.hitch(this, function () {
domConstruct.destroy(li);
this.itemsArray.splice(this.itemsArray.indexOf(textToAdd), 1);
if (this.itemsArray.length == 0) {
domClass.add(this.ulAdded, "hidden");
}
}));
The on-construct is a good one, but just testing this kind of construct might tell you whether that is the problem or not.
_addUser: function () {
.....
.....
dojo.connect(spanX, "click", this, this.spanClicked);
or
dojo.connect(spanX, "click", dojo.hitch(this, this.spanClicked);
},
spanClicked: function(args) {
domConstruct.destroy(li); //need to keep some reference to li
this.itemsArray.splice(this.itemsArray.indexOf(textToAdd), 1);
if (that.itemsArray.length == 0) {
domClass.add(this.ulAdded, "hidden");
}
}

My Dijit DateTimeCombo widget doesn't send selected value on form submission

i need to create a Dojo widget that lets users specify date & time. i found a sample implementation attached to an entry in the Dojo bug tracker. It looks nice and mostly works, but when i submit the form, the value sent by the client is not the user-selected value but the value sent from the server.
What changes do i need to make to get the widget to submit the date & time value?
Sample usage is to render a JSP with basic HTML tags (form & input), then
dojo.addOnLoad a function which selects the basic elements by ID, adds dojoType
attribute, and dojo.parser.parse()-es the page.
Thanks in advance.
The widget is implemented in two files. The application uses Dojo 1.3.
File 1: DateTimeCombo.js
dojo.provide("dojox.form.DateTimeCombo");
dojo.require("dojox.form._DateTimeCombo");
dojo.require("dijit.form._DateTimeTextBox");
dojo.declare(
"dojox.form.DateTimeCombo",
dijit.form._DateTimeTextBox,
{
baseClass: "dojoxformDateTimeCombo dijitTextBox",
popupClass: "dojox.form._DateTimeCombo",
pickerPostOpen: "pickerPostOpen_fn",
_selector: 'date',
constructor: function (argv) {},
postMixInProperties: function()
{
dojo.mixin(this.constraints, {
/*
datePattern: 'MM/dd/yyyy HH:mm:ss',
timePattern: 'HH:mm:ss',
*/
datePattern: 'MM/dd/yyyy HH:mm',
timePattern: 'HH:mm',
clickableIncrement:'T00:15:00',
visibleIncrement:'T00:15:00',
visibleRange:'T01:00:00'
});
this.inherited(arguments);
},
_open: function ()
{
this.inherited(arguments);
if (this._picker!==null && (this.pickerPostOpen!==null && this.pickerPostOpen!==""))
{
if (this._picker.pickerPostOpen_fn!==null)
{
this._picker.pickerPostOpen_fn(this);
}
}
}
}
);
File 2: _DateTimeCombo.js
dojo.provide("dojox.form._DateTimeCombo");
dojo.require("dojo.date.stamp");
dojo.require("dijit._Widget");
dojo.require("dijit._Templated");
dojo.require("dijit._Calendar");
dojo.require("dijit.form.TimeTextBox");
dojo.require("dijit.form.Button");
dojo.declare("dojox.form._DateTimeCombo",
[dijit._Widget, dijit._Templated],
{
// invoked only if time picker is empty
defaultTime: function () {
var res= new Date();
res.setHours(0,0,0);
return res;
},
// id of this table below is the same as this.id
templateString:
" <table class=\"dojoxDateTimeCombo\" waiRole=\"presentation\">\
<tr class=\"dojoxTDComboCalendarContainer\">\
<td>\
<center><input dojoAttachPoint=\"calendar\" dojoType=\"dijit._Calendar\"></input></center>\
</td>\
</tr>\
<tr class=\"dojoxTDComboTimeTextBoxContainer\">\
<td>\
<center><input dojoAttachPoint=\"timePicker\" dojoType=\"dijit.form.TimeTextBox\"></input></center>\
</td>\
</tr>\
<tr><td><center><button dojoAttachPoint=\"ctButton\" dojoType=\"dijit.form.Button\">Ok</button></center></td></tr>\
</table>\
",
widgetsInTemplate: true,
constructor: function(arg) {},
postMixInProperties: function() {
this.inherited(arguments);
},
postCreate: function() {
this.inherited(arguments);
this.connect(this.ctButton, "onClick", "_onValueSelected");
},
// initialize pickers to calendar value
pickerPostOpen_fn: function (parent_inst) {
var parent_value = parent_inst.attr('value');
if (parent_value !== null) {
this.setValue(parent_value);
}
},
// expects a valid date object
setValue: function(value) {
if (value!==null) {
this.calendar.attr('value', value);
this.timePicker.attr('value', value);
}
},
// return a Date constructed date in calendar & time in time picker.
getValue: function() {
var value = this.calendar.attr('value');
var result=value;
if (this.timePicker.value !== null) {
if ((this.timePicker.value instanceof Date) === true) {
result.setHours(this.timePicker.value.getHours(),
this.timePicker.value.getMinutes(),
this.timePicker.value.getSeconds());
return result;
}
} else {
var defTime=this.defaultTime();
result.setHours(defTime.getHours(),
defTime.getMinutes(),
defTime.getSeconds());
return result;
}
},
_onValueSelected: function() {
var value = this.getValue();
this.onValueSelected(value);
},
onValueSelected: function(value) {}
});
It sounds like you want to use getValue. The convention now is to use _getValueAttr and then call attr("value") but I think that started in Dojo 1.4 and this code would need to be ported to use those new patterns.
Noe that value should be a Javascript Date object which would best be sent to the server using dojo.date.stamp.toISOString()
This began to work fine after i added a "serialize" method to DateTimeCombo.js which builds exactly the output format i want.
This seems odd to me, since there is already a serialize implementation in _DateTimeTextBox.js that should output the value in the required ISO format.

looping through DOM / mootools sortables

I can't seem to get a handle on my list of sortables. They are a list of list elements, each with a
form inside, which I need to get the values from.
Sortables.implement({
serialize: function(){
var serial = [];
this.list.getChildren().each(function(el, i){
serial[i] = el.getProperty('id');
}, this);
return serial;
}
});
var sort = new Sortables('.teams', {
handle: '.drag-handle',
clone: true,
onStart: function(el) {
el.fade('hide');
},
onComplete: function(el) {
//go go gadget go
order = this.serialize();
alert(order);
for(var i=0; i<order.length;i++) {
if (order[i]) {
//alert(order[i].substr(5, order[i].length));
}
}
}
});
the sortables list is then added to a list in a loop with sort.addItems(li); . But when I try to get the sortables outside of the sortables onComplete declaration, js says this.list is undefined.
Approaching the problem from another angle:
Trying to loop through the DOM gives me equally bizarre results. Here are the firebug console results for some code:
var a = document.getElementById('teams').childNodes;
var b = document.getElementById('teams').childNodes.length;
try {
console.log('myVar: ', a);
console.log('myVar.length: ', b);
} catch(e) {
alert("error logging");
}
Hardcoding one li element into the HTML (rather than being injected via JS) changes length == 1, and allows me to access that single element, leading me to believe that accessing injected elements via the DOM is the problem (for this method)
Trying to get the objects with document.getElementById('teams').childNodes[i] returns undefined.
thank you for any help!
not sure why this would fail, i tried it in several ways and it all works
http://www.jsfiddle.net/M7zLG/ test case along with html markup
here is the source that works for local refernece, using the native built-in .serialize method as well as a custom one that walks the dom and gets a custom attribute rel, which can be your DB IDs in their new order (I tend to do that)
var order = []; // global
var sort = new Sortables('.teams', {
handle: '.drag-handle',
clone: true,
onStart: function(el) {
el.fade('hide');
},
onComplete: function(el) {
//go go gadget go
order = this.serialize();
}
});
var mySerialize = function(parentEl) {
var myIds = [];
parentEl.getElements("li").each(function(el) {
myIds.push(el.get("rel"));
});
return myIds;
};
$("saveorder").addEvents({
click: function() {
console.log(sort.serialize());
console.log(order);
console.log(mySerialize($("teams")));
}
});