I would like to create a custom widget that will display several widgets within it. For example, I would like a custom widget to be composed of a listview, a combobox, a calender, and a menu. Is this possible?
What I am thinking is adding the HTML in the refresh method, then initialize DOM elements such as below. I would like to use MVVM as well.
refresh: function() {
var that = this,
view = that.dataSource.view(),
html = kendo.render(that.template, view);
// trigger the dataBinding event
that.trigger(DATABINDING);
// mutate the DOM (AKA build the widget UI)
that.element.html(html);
// is this safe???
kendo.bind(that.element, { source: this.dataSource });
// or do this???
this.element.find('.listview').kendoListView({ dataSource: this.dataSource });
// trigger the dataBound event
that.trigger(DATABOUND);
}
It feels weird to call a kendo.bind in a widget where it is probably initialized via Kendo MVVM as well. Is there a better way to do this?
Yes it is possible, you have to create a plugin of yours which will generate the controls, You can refer the ways to create a plugin.
create basic jquery plugin
Below code is just a sample to help you start with, it is not a running code. You can modify this and make it run as per your requirement
I have created a sample for binding combobox, you can add up other controls in the same manner.
$.fn.customControl = function (options) {
var settings = $.extend({}, options);
return this.each(function () {
var $this = $(this);
var comboboxDatasource, listviewDatasource, menuDatasource; // as many controls you want to bind
$.each(settings.controls, function (index, value) {
switch (value.type) {
case "combobox":
comboboxDatasource = value.datasource;
var $combobox = "<input data-role='combobox' " +
" data-text-field='" + value.textField + "'" +
" data-value-field='" + value.valueField + "'" +
" data-bind='source: '" + value.datasource + "'," +
" events: {" +
" change: onChange," + //pass it in the custom control parameters if you want to have a custom event for the control
" }' />";
$this.append($combobox); // Appends a control to the DOM which can be later bound to data using MVVM kendo.observable
break;
case "listview":
//Create listview and other controls as demo'ed for the combobox.
break;
case "calendar":
break;
case "menu":
break;
}
});
//Create the kendo Observable object to bind the controls
var viewModel = kendo.observable({
comboboxDatasourceProperty: new kendo.data.DataSource({ //Fetch the datasource for each list control based on the parameters sent
transport: {
read: {
url: "url to datasource",
dataType: "dataType you want e.g. jsonp"
}
}
}),
listviewDatasourceProperty: function () { },
menuDatasourceProperty: function () { }
});
// Bind the View to the div which contains all the other controls
kendo.bind($($this), viewModel);
}); // return this.each
}; //customControl
Basic settings to use it is to create a div in the page which will actually contain all the other controls
<div id="customControlDiv"></div>
In the page you can use the control as below to create and bind the controls, if you want to bind it to the refresh function in observable then, write the below code within the refresh function
$("customControlDiv").customControl({ controls: [
{ type:'listview',id:'listviewID',datasource='path to datasource for listview',textField='text',valueField='id' }, //if you want to pass your datasource url, make a prop. and pass the url
{ type:'combobox',id:'comboboxID',datasource='path to datasource for combobox',textField='id',valueField='id' }, // which can be accessed in the plugin to fetch datasource
{ type:'calendar',:'calenderID',datasource='',textField='',valueField='' },
{ type:'menu',id:'menuID',datasource='path to datasource for menu',textField='text',valueField='id' }
]
});
Hope this helps :)
Related
I'm trying to pass function from MVC model to fullcalendar CustomButton like so
var model = new CalendarViewModel()
{
(...different properties of fullcalendar...)
CustomButtons = new
{
CustomButton = new
{
Text = "Custom",
Click = "function() { window.location.href = " + Url.Action("CustomView", "Custom") + "; }"
}
},
Header = new { Center = "title", Left = "prev,next customButton", Right = "month,agendaWeek,agendaDay,today" },
};
And then serialize and pass it to js file
function initFrom(calendarViewModel, rootUrl) {
$('#calendar').fullCalendar(calendarViewModel);
}
However I get error customButtonProps.click.call is not a function which I belive is caused because I'm passing string from serialized model.
If my approach is incorrect how can I achieve desired result - passing routing values to custom button click function (without hardcoding route values inside js file)?
I found a workaround for this.
Based on this scheduler demo I've appended custom button in JS and retrieved values from passed model (with a slight changes in model which I'm omitting here):
$('.fc-toolbar .fc-left').append(
$('<div class="fc-button-group"><button type="button" class="ui-button ui-state-default ui-corner-left ui-corner-right ui-state-hover">' + calendarViewModel.customButton.text + '</button></div>')
.on('click', function () {
location.href = calendarViewModel.customButton.url;
})
);
I'm using Template.rendered to setup a dropdown replacement like so:
Template.productEdit.rendered = function() {
if( ! this.rendered) {
$('.ui.dropdown').dropdown();
this.rendered = true;
}
};
But how do I re-run this when the DOM mutates? Helpers return new values for the select options, but I don't know where to re-execute my .dropdown()
I think you don't want this to run before the whole DOM has rendered, or else the event handler will run on EVERY element being inserted:
var rendered = false;
Template.productEdit.rendered = function() {rendered: true};
To avoid rerunning this on elements which are already dropdowns, you could give new ones a class which you remove when you make them into dropdowns
<div class="ui dropdown not-dropdownified"></div>
You could add an event listener for DOMSubtreeModified, which will do something only after the page has rendered:
Template.productEdit.events({
"DOMSubtreeModified": function() {
if (rendered) {
var newDropdowns = $('.ui.dropdown.not-dropdownified');
newDropdowns.removeClass("not-dropdownified");
newDropdowns.dropdown();
}
}
});
This should reduce the number of operations done when the event is triggered, and could stop the callstack from being exhausted
Here's my tentative answer, it works but I'm still hoping Meteor has some sort of template mutation callback instead of this more cumbersome approach:
Template.productEdit.rendered = function() {
if( ! this.rendered) {
$('.ui.dropdown').dropdown();
var mutationOptions = {
childList: true,
subtree: true
}
var mutationObserver = new MutationObserver(function(mutations, observer){
observer.disconnect(); // otherwise subsequent DOM changes will recursively trigger this callback
var selectChanged = false;
mutations.map(function(mu) {
var mutationTargetName = Object.prototype.toString.call(mu.target).match(/^\[object\s(.*)\]$/)[1];
if(mutationTargetName === 'HTMLSelectElement') {
console.log('Select Changed');
selectChanged = true;
}
});
if(selectChanged) {
console.log('Re-init Select');
$('.ui.dropdown').dropdown('restore defaults');
$('.ui.dropdown').dropdown('refresh');
$('.ui.dropdown').dropdown('setup select');
}
mutationObserver.observe(document, mutationOptions); // Start observing again
});
mutationObserver.observe(document, mutationOptions);
this.rendered = true;
}
};
This approach uses MutationObserver with some syntax help I found here
Taking ad educated guess, and assuming you are using the Semantic UI Dropdown plugin, there are four callbacks you can define:
onChange(value, text, $choice): Is called after a dropdown item is selected. receives the name and value of selection and the active menu element
onNoResults(searchValue): Is called after a dropdown is searched with no matching values
onShow: Is called after a dropdown is shown.
onHide: Is called after a dropdown is hidden.
To use them, give the dropdown() function a parameter:
$(".ui.dropdown").dropdown({
onChange: function(value, text, $choice) {alert("You chose " + text + " with the value " + value);},
onNoResults: function(searchValue) {alert("Your search for " + searchValue + " returned no results");}
onShow: function() {alert("Dropdown shown");},
onHide: function() {alert("Dropdown hidden");}
});
I suggest you read the documentation of all plugins you use.
I've found a couple other questions and resources here on using Bootstrap tooltips with a custom knockout binding handler. However, I haven't found is a cohesive solution that 1) involves using a dynamic knockout template 2) one where the tooltip can change when data it is bound to changes.
I'm also away of knockout-bootstrap on GitHub, but the tooltip title in that is rendered only once,
I've created a NEW JSFiddle with the following new dynamicTooltip that's based on the prior JSFiddle.
The new DynamicTooltip data binder looks like:
ko.renderTemplateHtml = function (templateId, data) {
var node = $("<div />")[0];
ko.renderTemplate(templateId, data, {}, node);
return $(node).html();
};
ko.bindingHandlers.tooltip = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var local = ko.utils.unwrapObservable(valueAccessor()),
options = {};
ko.utils.extend(options, ko.bindingHandlers.tooltip.options);
ko.utils.extend(options, local);
var tmplId = options.kotemplate;
ko.utils.extend(options, {
title: ko.renderTemplateHtml(tmplId, viewModel)
});
$(element).tooltip(options);
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).tooltip("destroy");
});
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var local = ko.utils.unwrapObservable(valueAccessor()),
options = {};
ko.utils.extend(options, ko.bindingHandlers.tooltip.options);
ko.utils.extend(options, local);
var tmplId = options.kotemplate;
var forceRefresh = options.forceRefresh;
var newdata = ko.renderTemplateHtml(tmplId, viewModel);
$(element).data('bs.tooltip').options.title = newdata
},
options: {
placement: "top",
trigger: "hover",
html: true
}};
It's not complete, as I'm manually triggering the update call manually by passing in a dummy databinding property on the view Model, in this case, it's called renderTooltip():
<a data-bind="tooltip: { title: firstName, placement: 'bottom', kotemplate: 'tile-tooltip-template', forceRefresh: renderTooltip() }">Hover on me</a>
I'd like to be able to trigger the tooltip to refresh itself when data changes.
I'm thinking I should be using createChildContext() and maybe controlsDescendantBindings, but I'm not really sure.
Any thoughts? I'll continue to update this, because it seems like dynamic bootstrap tooltips would be a common idea.
The root of the problem is that the update binding isn't firing, because it doesn't have a dependency on the properties that you are trying to update (i.e. firstName and address);
Normally you can delegate these properties to a new binding and let knockout automatically handle the dependency tracking. However, in this case, you're actually returning a string, so the element's automatic binding can't be used. A string is necessary, because that's how the tooltip works. If it could dynamically read from a DOM element, then the automatic bindings would work, but because it requires a HTML string, there's no way for the bindings to affect that.
Couple of options that I see:
1. Automatically create a dependency on the properties used by the template. This can be done by isolating the template view model (data) as seen in this fiddle: http://jsfiddle.net/tMbs5/13/
//create a dependency for each observable property in the data object
for(var prop in templateData)
if( templateData.hasOwnProperty(prop) && ko.isObservable(templateData[prop]))
templateData[prop]();
2. Instead of using an DOM-based template, use ko.computed to generate the template inside your view model. This will automatically create the dependencies as needed.
See this fiddle for an example of that: http://jsfiddle.net/tMbs5/12/
var vm = {
firstName: ko.observable('Initial Name'),
address: ko.observable('55 Walnut Street, #3'),
changeTooltip: function () {
this.firstName('New Name');
}
};
vm.tooltipHtml = ko.computed(function () {
return "<h2>" + vm.firstName() + "</h2>" +
"<span>" + vm.address() + "</span>";
});
ko.applyBindings(vm);
note: In both fiddles, I have refactored things a tiny bit - mostly for simplification
I'm using jQuery dataTables to display a table. I need to be able to pass a row selection event on to my Aura component that handles the selection and performs some operations on the data from that row.
In the initialize() function:
initialize: function()
{
$("#mytable tbody").click(function(event)
{
$(mytable.fnSettings().aoData).each(function ()
{
$(this.nTr).removeClass('row_selected');
});
$(event.target.parentNode).addClass('row_selected');
});
mytable = $('#mytable').dataTable();
},
I set up the click handler for the row selection, but how do I get a reference to the enclosing component so I can sandbox.emit() function to issue messages? I can put a reference to the component into the Closure, but that essentially makes this component a singleton and I could never have two instances of the component on the page at the same time.
Is there a standard way, using jQuery selectors or some other method, that I can retrieve a reference to the enclosing component from inside the click() handler?
Edit: I should never try to write code until I have had 32oz of caffine. You can pass a reference to the current component via the click() method itself. Like so:
$("#mytable tbody").click(this, function(event)
{
$(mytable.fnSettings().aoData).each(function ()
{
$(this.nTr).removeClass('row_selected');
});
$(event.target.parentNode).addClass('row_selected');
event.data.sandbox.emit('mychannel', {data: 'stuff'});
});
If I understand your question correctly, you could try something like this
initialize: function () {
var that = this;
$("#mytable tbody").click(function(event) {
//have acces to component as 'that'
});
}
what I used for events is view inside component configuration:
View: {
events: {
'click a[data-question-edit-id]': function (e) {
var button = $(e.currentTarget),
id = button.attr('data-question-edit-id'),
examId = this.component.examModel.get('id');
this.sandbox.router.navigate('/exams/' + examId + '/questions/' + id + '/edit', {trigger: true});
},
'click a[data-question-delete-id]': function (e) {
var button = $(e.currentTarget),
id = button.attr('data-question-delete-id');
this.component.showDeleteConfirmation(id);
}
}
}
If you'll find be helpful, here is my repo of aura project I'm working on:
https://github.com/lyubomyr-rudko/aura-test-project
I'm trying to bind an onChange event of one FilteringSelect to populate another FilteringSelect.
// View
dojo.addOnLoad(function () {
dojo.connect(dijit.byId('filterselect1'), 'onChange', function () {
dijit.byId('filterselect2').store = new dojo.data.ItemFileReadStore(
{ url: "/test/autocomplete/id/" + dijit.byId("filterselect1").value }
);
});
});
The JSON is generated from what I can tell correctly from a Zend Action Controller using a autoCompleteDojo helper.
// Action Controller
public function autocompleteAction()
{
$id = $this->getRequest()->getParam('id');
$select = $this->_table->select()
->from($this->_table, array('id','description'))
->where('id=?',$id);
$data = new Zend_Dojo_Data('id', $this->_table->fetchAll($select)->toArray(), 'description');
$this->_helper->autoCompleteDojo($data);
}
I receive the JSON from the remote datastore correctly, but it does not populate the second FilteringSelect. Is there something else I need to do to push the JSON onto the FilteringSelect?
I couldn't believe this was causing the problem, but the whole issue boiled down to the fact that it appears that a dojo ItemFileReadStore REQUIRES the label property of the JSON to be "name". In the end this is all that it required to wire them together.
dojo.addOnLoad(function () {
dijit.byId('filtering_select_2').store = new dojo.data.ItemFileReadStore({url: '/site/url'});
dojo.connect(dijit.byId('filtering_select_1'), 'onChange', function (val) {
dijit.byId('filtering_select_2').query.property_1 = val || "*";
});
});
UPDATE: The property within Zend form has been fixed as of ZF 1.8.4
Try console.log() in the event to see if it is launched. Changing the store should work, however for other widgets like grid you have also to call refreshing methods.