For a random experiment, I was trying to build a shopping list, using Stencil. My input would be a string, ie. "cauliflower, cucumber" with ingredients on the shopping list. I would like this string to be converted in an unordered list.
Converting the string to an array is no problem. Going from the array to the list, I tried using a for loop, looping through all options in the array and adding them to the list.
export class ShoppingList {
#Prop() veggies: string;
private makeList(typeString: string): HTMLUListElement {
var listArray: string[] = typeString.split(',');
for(var i=0; i<listArray.length; i++) {
// Create the list element:
var list = document.createElement('ul');
// Create the list item:
var item = document.createElement('li');
// Set its contents:
item.appendChild(document.createTextNode(listArray[i]));
// Add it to the list:
list.appendChild(item);
}
return list;
}
private getVeggies(): HTMLUListElement {
return this.makeList(this.veggies);
}
render() {
return (
<div>
veggies:
<ul>{this.getVeggies()}</ul>
</div>
);
}
}
The index.html file runs this code through the following line:
<shopping-list veggies="Cauliflower, cucumber" ></shopping-list>
I expected an output of veggies: * cauliflower \n *cucumber. Instead, I just get veggies: with an empty list. When inspecting the elements, I get the following HTML output:
<div>veggies:<ul><undefined></undefined> </ul><br></div>
You are mixing DOM nodes and VDOM nodes. With the VDOM you can remove the code manually creating DOM nodes:
export class ShoppingList {
#Prop() veggies: string;
private makeList(typeString: string): HTMLUListElement {
var listArray: string[] = typeString.split(',');
return (
<ul>
{listArray.map((item) => <li>{item}</li>)}
</ul>
);
private getVeggies(): HTMLUListElement {
return this.makeList(this.veggies);
}
render() {
return (
<div>
veggies:
{this.getVeggies()}
</div>
);
}
}
First of all, I changed makeList so it returns VDOM and loop over the items with map instead of a for loop. Also I removed the <ul> from the render() method since makeList already returns it.
Related
We're trying to write a 'paragraph' model downcast converter that will wrap all text nodes in a span, inside the paragraph p block element.
For example we have the following:
function AddSpansToText(editor) {
editor.conversion.for('downcast').add(dispatcher => {
dispatcher.on('insert:paragraph', (evt, data, conversionApi) => {
// Remember to check whether the change has not been consumed yet and consume it.
if (!conversionApi.consumable.consume(data.item, 'insert')) {
return;
}
const { writer, mapper } = conversionApi
// Translate the position in the model to a position in the view.
const viewPosition = mapper.toViewPosition(data.range.start);
// Create a <p> element that will be inserted into the view at the `viewPosition`.
const div = writer.createContainerElement('p', { class: 'data-block' });
const span = writer.createAttributeElement('span', { class: 'data-text' });
writer.insert(writer.createPositionAt(div, 0), span);
// Bind the newly created view element to the model element so positions will map accordingly in the future.
mapper.bindElements(data.item, div);
// Add the newly created view element to the view.
writer.insert(viewPosition, div);
// Remember to stop the event propagation.
evt.stop();
});
});
}
We then register the function above as an extra plugin in the config settings as...
extraPlugins: [AddSpansToText],
This is close, however, we're not able to get the text node to appear inside the span, it appears as a peer, as ...
<p>
Text here....
<span></span>
</p>
We can't seem to map the model to the new view position.
Suggestions as to what we might be doing wrong greatly appreciated.
For anyone else looking for this, and based loosely on this example here... https://ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/conversion/custom-element-conversion.html
... here's what I've come up with...
/**
* Helper method to map model to view position
*
* #param {*} view
*/
function createModelToViewPositionMapper(view) {
return (evt, data) => {
const modelPosition = data.modelPosition;
const parent = modelPosition.parent;
// Only the mapping of positions that are directly in
// the <paragraph> model element should be modified.
if (!parent || !parent.is('element', 'paragraph')) {
return;
}
// Get the mapped view element <div class="data-block">.
const viewElement = data.mapper.toViewElement(parent);
// Find the <span class="data-text"> in it.
const viewContentElement = findContentViewElement( view, viewElement );
// Translate the model position offset to the view position offset.
data.viewPosition = data.mapper.findPositionIn( viewContentElement, modelPosition.offset );
};
}
/**
* Helper method to find child span at correct curser offset
*
* #param {*} editingView
* #param {*} viewElement
* #returns <span class="data-text"> nested in the parent view structure.
*/
function findContentViewElement( editingView, viewElement ) {
for ( const value of editingView.createRangeIn( viewElement ) ) {
if ( value.item.is( 'element', 'span' ) && value.item.hasClass( 'data-text' ) ) {
return value.item;
}
}
}
/**
* Paragraph model downcast converter to wrap all text nodes in
* inline span elements
*
* #param {*} editor
*/
function ParagraphConverter(editor) {
editor.conversion.for('downcast').add(dispatcher => {
dispatcher.on('insert:paragraph', (evt, data, conversionApi) => {
// Remember to check whether the change has not been consumed yet and consume it.
if (!conversionApi.consumable.consume(data.item, 'insert')) {
return;
}
const { writer, mapper } = conversionApi
// Translate the position in the model to a position in the view.
const viewPosition = mapper.toViewPosition(data.range.start);
// Create a <div> element that will be inserted into the view at the `viewPosition`.
const div = writer.createContainerElement('div', { class: 'data-block' });
// Create the <span> element that will be inserted into the div
const span = writer.createEditableElement('span', { class: 'data-text' });
writer.insert(writer.createPositionAt(div, 0), span);
// Bind the newly created view element to the model element so positions will map accordingly in the future.
mapper.bindElements(data.item, div);
// Add the newly created view element to the view.
writer.insert(viewPosition, div);
// Remember to stop the event propagation.
evt.stop();
});
});
// Dynamic mapping for model to view and curser position with correct offset
editor.editing.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
editor.data.mapper.on( 'modelToViewPosition', createModelToViewPositionMapper( editor.editing.view ) );
}
I have a function:
checkWebElemAndAssert(...elements) {
for (const element of elements) {
element.should('be.visible').click().should('be.checked');
}
}
and i use it within another function:
checkRegisterValues = () => {
let maleCheckBox = cy.get('input[value=Male]');
let femaleCheckBox = cy.get('input[value=FeMale]');
let cricketCheckBox = cy.get('#checkbox1');
let registerElemList = [maleCheckBox, femaleCheckBox, cricketCheckBox];
this.browserUtils.checkWebElemAndAssert(...registerElemList);
return this;
}
The problem is that when i use checkRegisterValues() it uses for each action the last element: cricketCheckBox. Any hints on what is wrong? i would expect that the action is made for each element and not the last one.
Have you tried passing in the array like this?
this.browserUtils.checkWebElemAndAssert(registerElemList);
You can also print out
checkWebElemAndAssert(...elements) {
console.log(elements);
for (const element of elements) {
element.should('be.visible').click().should('be.checked');
}
}
and see what you are passing in
ok so i read a bit more and made this:
checkWebElemAndAssert2(elements) {
cy.get(elements).each(($list) => {
cy.get($list).click({ multiple: true }).should('be.checked')
})
}
Basically in elements when i call checkWebElemAndAssert2 i will give the list identifier. Seems to work but not sure meets the standard.
checkRegisterValues = () => {
let myList = 'input[type=radio]';
this.browserUtils.checkWebElemAndAssert2(myList);
return this;
}
I'm trying to add a simple drop down control above a list such that I can sort it by "created" or "title".
The list template is called posts_list.html. In it's helper .js file I have:
posts: function () {
var sortCriteria = Session.get("sortCriteria") || {};
return Posts.find({},{sort: {sortCriteria: 1}});
}
Then, I have abstracted the list into another template. From here I have the following click event tracker in the helper.js
"click": function () {
// console.log(document.activeElement.id);
Session.set("sortCriteria", document.activeElement.id);
// Router.go('history');
Router.render('profile');
}
Here I can confirm that the right Sort criteria is written to the session. However, I can't make the page refresh. The collection on the visible page never re-sorts.
Frustrating. Any thoughts?
Thanks!
You can't use variables as keys in an object literal. Give this a try:
posts: function() {
var sortCriteria = Session.get('sortCriteria');
var options = {};
if (sortCriteria) {
options.sort = {};
options.sort[sortCriteria] = 1;
}
return Posts.find({}, options);
}
Also see the "Variables as keys" section of common mistakes.
thanks so much for that. Note I've left commented out code below to show what I pulled out. If I required a truly dynamic option, versus the simply binary below, I would have stuck w/ the "var options" approach. What I ended up going with was:
Template.postList.helpers({
posts: function () {
//var options = {};
if (Session.get("post-list-sort")) {
/*options.sort = {};
if (Session.get("post-list-sort") == "Asc") {
options.sort['created'] = 1;
} else {
options.sort['created'] = -1;
}*/
//return hunts.find({}, options);}
console.log(Session.get("hunt-list-sort"));
if (Session.get("hunt-list-sort") == "Asc") {
return Hunts.find({}, {sort: {title: 1}});
}
else {
return Hunts.find({}, {sort: {title: -1}});
};
}
}
});
I'll start off by stating that I don't know if this is possible at all, but I'm reading over the Kendo UI documentation and trying to figure out how to at least try it, but I'm running into a lot of difficulties with making a custom binding. This is a followup to another question I am still working on, which is posted here. If this is not an appropriate question, please kindly let me know, and I will close it or rephrase it. I'm just really lost and confused at this point.
As I understand it, based on what I've been told and tried, Kendo cannot bind to an Associative Array not because the data isn't good, but because it is an array of objects, each as a separate individual entity - under normal circumstances, an array would be a bit different and contain a length property, as well as some other functions in the array prototype that make iteration through it possible.
So I was trying to conjecture how to get around this. I succeeded in getting what I think was a workaround to function. I preface that with "think" because I'm still too inexperienced with Javascript to truly know the ramifications of doing it this way (performance, stability, etc)
Here is what I did;
kendo template
<script type="text/x-kendo-template" id="display-items-many">
# for(var key in data) { #
# if (data.hasOwnProperty(key) && data[key].hasOwnProperty("Id")) { #
<tr>
<td>
<strong>#= data[key].Id #</strong>
</td>
<td class="text-right">
<code>#= data[key].Total #</code>
</td>
</tr>
# } #
# } #
</script>
html
<table class="table borderless table-hover table-condensed" data-bind="source: Associative data-template="display-items-many">
</table>
Now to me, immediately off hand, this gave me the illusion of functioning. So I got to thinking a bit more on how to fix this ...
I want to create a new binding called repeat. The goal of this binding is as follows;
repeat the template for each instance of an object within the given root object that meets a given criteria
In my head, this would function like this;
<div data-template="repeater-sample" data-bind="repeat: Associative"></div>
<script type="text/x-kendo-template" id="repeater-sample">
<div> ${ data.Id }</div>
</script>
And the criteria would be a property simply called _associationKey. So the following would, in theory, work.
$.ajax({
// get data from server and such.
}).done(function(results){
// simple reference to the 'associative array' for easier to read code
var associative = results.AssociativeArray;
// this is a trait that everything in the 'associative array' should have to match
// this is purely, purely an example. Obviously you would use a more robust property
var match = "Id";
// go through the results and wire up the associative array objects
for(var key in associative ) {
if(associative.hasOwnProperty(key) && associative[key].hasOwnProperty(match)) {
associative[key]._associationKey = 10; // obviously an example value
}
}
// a watered down example implementation, obviously a real use would be more verbose
viewModel = kendo.observable({
// property = results.property
// property = results.property
associativeArray = associative
});
kendo.bind('body', viewModel);
});
So far this actually seems to work pretty well, but I have to hard code the logic in the template using inline scripting. That's kind of what I want to avoid.
Problem
The big issue is that I'm vastly confused on telerik's documentation for custom bindings (available here). I do have their examples to draw from, yes - but it's a bit confusing to me how it interacts with the object. I'll try to explain, but I'm so lost that it may be difficult.
This is what telerik gives for an example custom binding, and I've pruned it a bit for space concerns;
<script>
kendo.data.binders.repeater = kendo.data.Binder.extend({
init: function(element, bindings, options) {
//call the base constructor
kendo.data.Binder.fn.init.call(this, element, bindings, options);
var that = this;
// how do we interact with the data that was bound?
}
});
</script>
So essentially that's where I am lost. I'm having a big disconnect figuring out how to interact with the actual "associative array" that is bound using data-bind="repeat: associativeArray"
So ..
I need to interact with the bound data (the entire 'associative array')
I need to be able to tell it to render the target template for each instance that matches
Further Updates
I have been digging through the kendo source code, and this is what I have so far - by taking the source binding as an example... but I'm still not getting the right results. Unfortunately this poses a few problems;
some of the functions are internal to kendo, I'm not sure how to get access to them without re-writing them. While I have the source and can do that, I'd prefer to make version agnostic code so that it can "plug in" to newer releases
I'm totally lost about what a lot of this does. I basically made a copy of the source binding and replaced it with my own syntax where possible, since the concept is fundamentally the same. I cannot figure out where to do the test for qualification to be rendered, if that makes sense.
I'm having a big logic disconnect here - there should ideally be some place where I can basically say ... If the current item that kendo is attempting to render in a template matches a criteria, render it. If not, pass it over and then another place where I tell it to iterate over every object in the 'associative array' so as to get to the point where I test it.
I feel just forcing a for loop in here will actually make this fire too many times, and I am getting pretty lost. Any help is greatly appreciated.
kendo.data.binders.repeat = kendo.data.Binder.extend({
init: function(element, bindings, options) {
kendo.data.Binder.fn.init.call(this, element, bindings, options);
var source = this.bindings.repeat.get();
if (source instanceof kendo.data.DataSource && options.autoBind !== false) {
source.fetch();
}
},
refresh: function(e) {
var that = this,
source = that.bindings.repeat.get();
if (source instanceof kendo.data.ObservableArray|| source instanceof kendo.data.DataSource) {
e = e || {};
if (e.action == "add") {
that.add(e.index, e.items);
} else if (e.action == "remove") {
that.remove(e.index, e.items);
} else if (e.action != "itemchange") {
that.render();
}
} else {
that.render();
}
},
container: function() {
var element = this.element;
if (element.nodeName.toLowerCase() == "table") {
if (!element.tBodies[0]) {
element.appendChild(document.createElement("tbody"));
}
element = element.tBodies[0];
}
return element;
},
template: function() {
var options = this.options,
template = options.template,
nodeName = this.container().nodeName.toLowerCase();
if (!template) {
if (nodeName == "select") {
if (options.valueField || options.textField) {
template = kendo.format('<option value="#:{0}#">#:{1}#</option>',
options.valueField || options.textField, options.textField || options.valueField);
} else {
template = "<option>#:data#</option>";
}
} else if (nodeName == "tbody") {
template = "<tr><td>#:data#</td></tr>";
} else if (nodeName == "ul" || nodeName == "ol") {
template = "<li>#:data#</li>";
} else {
template = "#:data#";
}
template = kendo.template(template);
}
return template;
},
add: function(index, items) {
var element = this.container(),
parents,
idx,
length,
child,
clone = element.cloneNode(false),
reference = element.children[index];
$(clone).html(kendo.render(this.template(), items));
if (clone.children.length) {
parents = this.bindings.repeat._parents();
for (idx = 0, length = items.length; idx < length; idx++) {
child = clone.children[0];
element.insertBefore(child, reference || null);
bindElement(child, items[idx], this.options.roles, [items[idx]].concat(parents));
}
}
},
remove: function(index, items) {
var idx, element = this.container();
for (idx = 0; idx < items.length; idx++) {
var child = element.children[index];
unbindElementTree(child);
element.removeChild(child);
}
},
render: function() {
var source = this.bindings.repeat.get(),
parents,
idx,
length,
element = this.container(),
template = this.template();
if (source instanceof kendo.data.DataSource) {
source = source.view();
}
if (!(source instanceof kendo.data.ObservableArray) && toString.call(source) !== "[object Array]") {
source = [source];
}
if (this.bindings.template) {
unbindElementChildren(element);
$(element).html(this.bindings.template.render(source));
if (element.children.length) {
parents = this.bindings.repeat._parents();
for (idx = 0, length = source.length; idx < length; idx++) {
bindElement(element.children[idx], source[idx], this.options.roles, [source[idx]].concat(parents));
}
}
}
else {
$(element).html(kendo.render(template, source));
}
}
});
I would propose as a simpler solution transform transmitted associative array in an array. This is pretty simple and (for most cases) can solve your problem.
Lets say that you get the following associative array received from the server:
{
"One" : { Name: "One", Id: "id/one" },
"Two" : { Name: "Two", Id: "id/two" },
"Three" : { Name: "Three", Id: "id/three" }
}
That is store in a variable called input. Transform it from associative to no associative is as easy as:
var output = [];
$.each(input, function(idx, elem) {
elem.index = idx;
output.push(elem);
});
Now, you have in output an equivalent array where I saved the index field into a field called index for each element of the associative array.
Now you can use out-of-the-box code for displaying the data received from the server.
See it in action here : http://jsfiddle.net/OnaBai/AGfWc/
You can even use KendoUI DataSource for retrieving and transforming the data by using DataSource.schema.parse method as:
var dataSource = new kendo.data.DataSource({
transport: {
read: ...
},
schema : {
parse: function (response) {
var output = [];
$.each(response, function(idx, elem) {
elem.index = idx;
output.push(elem);
});
return output;
}
}
});
and your model would be:
var viewModel = new kendo.data.ObservableObject({
Id: "test/id",
Associative: dataSource
});
You can see it in action here: http://jsfiddle.net/OnaBai/AGfWc/1/
I've searched everywhere to find out how to add a class to a particular row in slickgrid. It looks like there used to be a rowCssClasses property but it's gone now. Any help on this would be extremely appreciated.
Update: I figured it out using the getItemMetadata...so before you render, you have to do something like this:
dataView.getItemMetadata = function (row) {
if (this.getItem(row).compareThis > 1) {
return {
'cssClasses': 'row-class'
};
}
};
That will inject that 'row-class' into the row that matches the if statement. It seems that this getItemMetadata function doesn't exist until you put it there and slickGrid checks to see if there's anything in there. It makes it kind of difficult to figure out it's options but if you search for getItemMetadata in the slick.grid.js file you should find some hidden treasures! I hope this helps someone!
If there's a better way of doing this, please let me know.
In newer versions of SlickGrid, DataView brings its own getItemMetadata to provide formatting for group headers and totals. It is easy to chain that with your own implementation though. For example,
function row_metadata(old_metadata_provider) {
return function(row) {
var item = this.getItem(row),
ret = old_metadata_provider(row);
if (item && item._dirty) {
ret = ret || {};
ret.cssClasses = (ret.cssClasses || '') + ' dirty';
}
return ret;
};
}
dataView.getItemMetadata = row_metadata(dataView.getItemMetadata);
myDataView.getItemMetadata = function(index)
{
var item = myDataView.getItem(index);
if(item.isParent === true) {
return { cssClasses: 'parentRow' };
}
else {
return { cssClasses: 'childRow' };
}
};
//In my CSS
.parentRow {
background-color: #eeeeee;
}
.childRow {
background-color: #ffffff;
}
You could use the setCellCssStyles function:
https://github.com/mleibman/SlickGrid/wiki/Slick.Grid#wiki-setCellCssStyles
grid.setCellCssStyles(key, hash)
key - A string key. Will overwrite any data already associated with
this key.
hash - A hash of additional cell CSS classes keyed by row number and
then by column id. Multiple CSS classes can be specified and separated
by space.
Example:
{
0: {
"number_column": "cell-bold",
"title_column": "cell-title cell-highlighted"
},
4: {
"percent_column": "cell-highlighted"
} }
I used that to highlight edited fields in my grid. I didn't like the getItemMetadata method.