My application uses CKEditor 5 to allow users to edit rich text data. These texts support some application-specific custom elements (Web Components), and I want to extend CKEditor with custom plugins that support inserting such custom elements. I seem to be almost there, but I'm having some difficulties getting these custom elements into and out of the CKEditor instance properly.
Current plugin implementations
I mainly followed the Implementing an inline widget tutorial from the CKEditor 5 documentation. As an example, I would like to support a custom element like <product-info product-id="123"></product-info>, which in CKEditor should be rendered as a simple <span> with a specific class for some styling.
In my editing plugin, I first define the extension to the schema:
const schema = this.editor.model.schema;
schema.register('product-info', {
allowWhere: '$text',
isInline: true,
isObject: true,
allowAttributes: [ 'product-id' ]
});
I then define the upcast and downcast converters, closely sticking to the tutorial code:
const conversion = this.editor.conversion;
conversion.for('upcast').elementToElement({
view: {
name: 'span',
classes: [ 'product-info' ]
},
model: (viewElement, { writer: modelWriter }) => {
const id = viewElement.getChild(0).data.slice(1, -1);
return modelWriter.createElement('product-info', { 'product-id': id });
}
});
conversion.for('editingDowncast').elementToElement({
model: 'product-info',
view: (modelItem, { writer: viewWriter }) => {
const widgetElement = createProductInfoView(modelItem, viewWriter);
return toWidget(widgetElement, viewWriter);
}
});
conversion.for('dataDowncast').elementToElement({
model: 'product-info',
view: (modelItem, { writer: viewWriter }) => createProductInfoView(modelItem, viewWriter)
});
function createProductInfoView(modelItem, viewWriter) {
const id = modelItem.getAttribute('product-id');
const productInfoView = viewWriter.createContainerElement(
'span',
{ class: 'product-info' },
{ isAllowedInsideAttributeElement: true }
);
viewWriter.insert(
viewWriter.createPositionAt(productInfoView, 0),
viewWriter.createText(id)
);
return productInfoView;
}
Expected behavior
The idea behind all this is that I need to support the custom <product-info> elements stored in user data in the backend. CKEditor, which is used by users to edit that data, should load these custom elements and transform them into a styled <span> for display purposes while editing. These should be treated as inline widgets since they should only be able to be inserted, moved, copied, pasted, deleted as a whole unit. A CKEditor plugin should allow the user to create new such elements to be inserted into the text, which will then also be <span>s in the editing view, but <product-info>s in the model, which should also be written back to the backend database.
In other words, I expected this to ensure a direct mapping between element <product-info product-id="123"></product-info> in the model, and <span class="product-info">123</span> in the view, to support inserting and moving of <product-info> elements by the user.
Actual result
In short, I seem to be unable to get CKEditor to load data containing <product-info> elements, and unable to retrieve the model representation of these custom elements for backend storage. All operations to insert data to CKEditor from source, or to retrieve CKEditor data for sending to the backend, seem to operate on the view.
For example, if I preload CKEditor contents either by setting the inner content of the element that is replaced with the editor instance, or inserting it like this:
const viewFragment = editor.data.processor.toView(someHtml);
const modelFragment = editor.data.toModel(viewFragment);
editor.model.insertContent(modelFragment);
I see the following behavior (verified using CKEditor Inspector):
When inserting the custom element, i.e. <product-info product-id="123"></product-info>, the element is stripped. It's not present in either the model nor the view.
When inserting the view representation, i.e. <span class="product-info">123</span> I get the representation that I want, i.e. that same markup in CKEditor's view, and the <product-info product-id="123"></product-info> tag in the model.
This is exactly the opposite of what I want! In my backend, I don't want to store the view representation that I created for editing purposes, I want to store the actual custom element. Additionally:
My UI plugin to insert new product info elements, uses a command that does the following:
execute({ value }) {
this.editor.model.change( writer => {
const productInfo = writer.createElement('product-info', {
'product-id': value
});
this.editor.model.insertContent(productInfo);
writer.setSelection(productInfo, 'on');
});
}
which also works as I want it to, i.e. it generates the product-info tag for the model and the span for the view. But, of course, when loading an entire source text when initialising the editor with data from the backend, I can't use this createElement method.
Conversely, in order to retrieve the data from CKEditor for saving, my application uses this.editor.getData(). There, these proper pairs of <product-info> model elements and <span> view elements get read out in their view representation, instead of their model representation – not what I want for storing this data back!
The question
My question is: what do I need to change to be able to load the data into CKEditor, and get it back out of the CKEditor, using the custom element, rather than the transformed element I want to show only for editing purposes? Put differently: how can I make sure the content I insert into CKEditor is treated as the model representation, and how do I read out the model representation from my application?
I'm confused about this because if the model representation is something that is only supposed to be used internally by CKEditor, and not being able to be set or retrieved from outside – then what is the purpose of defining the schema and these transformations in the first place? It will only ever be visible to CKEditor, or someone loading up the CKEditor Inspector, but of no use to the application actually integrating the editor.
Sidenote: an alternative approach
For a different approach, I tried to forgo the transformation to <span>s entirely, and just use the custom element <product-info>, unchanged, in both the model and the view, by using the General HTML Support functionality. This worked fine, since this time no transformation was needed, all I had to do was to set the schema in order for CKEditor to accept and pass through the custom elements.
The reason I can't go with this approach is that in my application, these custom components are handled using Angular Elements, so they will actually be Angular components. The DOM manipulation seems to interfere with CKEditor, the elements are no longer treated as widgets, and there are all manner of bugs and side effects that come with it. Elements show up fine in CKEditor at first, but things start falling apart when trying to select or move them. Hence my realisation that I probably need to create a custom representation for them in the CKEditor view, so they're not handled by Angular and preventing these issues.
I am trying to use a syncfusion ej-grid to auto generate a grid using data retrieved from an ajax call. In doing so, I am running into a problem.
I am getting an error message when the page first renders saying "DataSource must not be empty at initial load since columns are generated from dataSource in AutoGenerate Column Grid". While I understand this, I am struggling because I don't know anything about the datasource until after the ajax call is returned.
This is a .NET CORE 5 razor page....
My razor page:
(note: 'data' below is a json serialized System.Data.DataTable).
<ej-grid id="gridResults" allowPaging="true"></ej-grid>
<script type="text/javascript>
function ajaxResponse(data) {
$('#gridResults').ejGrid({
datasource: data
});
}
</script>
So my questions are two-fold:
First, is there a way to bypass the initial error I am getting about the datasource not being set (since I don't have a datasource yet)...
Second, Will my approach of providing a serialized DataTable work, or do I need to do something different to pass the data via Javascript like this?
Right now when I return from the ajax call, I don't get any errors, but nothing happens...
Thanks in advance!
Greetings from Syncfusion Support.
Query 1: I am getting an error message when the page first renders saying "DataSource must not be empty at initial load since columns are generated from dataSource in AutoGenerate Column Grid".
This issue will occur when we do not bind dataSource and define columns to the Grid. For rendering Grid, we should either define columns or bind data source at the initial rendering.
Since you are not binding data in initial rendering, we suggest you to define columns so that Grid will be rendered without any errors.
Query 2: Will my approach of providing a serialized DataTable work, or do I need to do something different to pass the data via Javascript like this?
For binding data table to Grid after serializing it, we should call the dataSource method of grid and pass the data as parameter to it as shown in below code,
<ej-button id="repeatButton" text="click" size="Medium" show-rounded-corner="true" click="btnClick" />
<ej-grid id="FlatGrid" allow-paging="true">
<e-columns>
<e-column field="No" header-text="Order ID" is-primary-key="true"></e-column>
<e-column field="Name" header-text="Employee ID"></e-column>
</e-columns>
</ej-grid>
<script>
function btnClick(args) {
$.ajax({
url: '/Grid/DataSource',
dataType: "json",
type: 'POST',
success: function (data) {
var grid = $('.e-grid').data("ejGrid"); // grid instance
grid.dataSource(data);
// update grid data source by calling dataSource method and passing data
}
})
}
</script>
[HttpPost]
public ActionResult DataSource()
{
System.Data.DataTable dt = new DataTable("Table1");
DataColumn cl = new DataColumn("No");
dt.Columns.Add(cl);
cl = new DataColumn("Name");
dt.Columns.Add(cl);
DataRow dataRow = dt.NewRow();
dataRow[0] = 1;
dataRow[1] = "John";
dt.Rows.Add(dataRow);
var data1 = Utils.DataTableToJson(dt); // method to serialize data table
return Json(data1); // fetching and returning boolean column
}
In the above code example, we have fetched data using ajax call in button click and bind it to grid using dataSource method.
Please check the below API help documentation,
https://help.syncfusion.com/api/js/ejgrid#methods:datasource
Kindly get back to us for further assistance.
Regards,
Padmavathy Kamalanathan
Is it possible to identify on a kendo UI grid which field was altered on a row edit?
Right now I am sending the entire row to the server that was changed. I would like to send
the request to the server to also include a variable holding the name
of the field that was edited.
Is something like that supported by kendo or
is there a work around for that?
This is not supported out of the box. However the grid API should allow this to be implemented. Check the edit and save events. In there you can listen to changes of the model instance which is currently being edit. Here is a quick example:
$("#grid").kendoGrid({
edit: function(e) {
e.model.unbind("change", model_change).bind("change", model_change);
}
});
function model_change(e) {
var model = this;
var field = e.field;
// store somewhere the field and model
}
Let´s say I have some sort of datagrid and I want to add a couple chained filters like in this site:
http://www.yelp.com/search?find_desc=bar&ns=1&find_loc=Minneapolis%2C+MN
(sort by,distance,price etc).
Each time a user clciked in a filter link it will update the content of datagrid accordingly. But I would also need to update the links in other filters to take account of the changes. Ex: if i change the order field I need to add/update ?order_field=x in all the other filters links.
What you think is the best way to implement such scenario?
Should i create a function that, when a filter link is clicked, it update the query string params of all the other filters? Or use hidden fields to record the selected option in each filter?
I would like a reusable solution if possible.
Since the data is loading via AJAX, there shouldn't be any links to update - at least not if you mean anchor tags <a>. You don't even need to store the filters in a hidden field.
I would store all the filters as a JSON object. Depending on how your API is set up, you may have to convert the JSON object to something usable by your API or you may even be able to pass on the JSON object directly in the $.ajax request.
This sample code assumes you have a textbox with id="price" in the markup. I intentionally left convert_filters_to_parameters blank because you didnt provide any details as to your API. jQuery will in turn serialize those parameters into a GET or POST request before it sends them out.
var filters = {
distance:null,
price:null,
sortBy:'distance'
}
//this assumes you have a textbox with id="price"
$('#price').changed(function()
{
filters.price = $(this).val();
refresh_data();
});
function refresh_data()
{
var parameters = convert_filters_to_parameters(filters);
$.ajax('/my_api',
{
//i left out a lot of properties here for brevity
data: parameters,
success: function(response) { alert(response); }
});
}
I have added a rad combobox to a page and need to be able to get the selected value from it. I can see below the html element a hidden field called 'mycontrol_ClientState' I take it this is where I am meant to retrieve the value from however I dont know how to.
Does anyone have an example of getting the value out clientside?
Use the get_value() JavaScript method:
var combobox = $find("<%= RadComboBox1.ClientID %>");
var value = combobox.get_value();