I have a tree-like setup, where each node contains its own Mustache template that might be wrapped in a parent node's template.
var templates = {
secondTmpl: Mustache.compile("<i>Yet another template.. Here's some text: {{text}}</i> {{date}}"),
template: Mustache.compile("<b>{{val}}</b><p>{{{outlet}}}</p><ul>{{#list}}<li>{{.}} </li>{{/list}}</ul> {{test}}")
};
var tree = [
{
template: "template",
data: { val: "yup!!", list: [1,2,3,4,5], test: function() { return new Date(); } },
children: [
{
view: "Main",
template: "secondTmpl",
data: { text: "Some Value", date: function() { return new Date(); } }
}
]
}
];
function MainView(options) {
this.template = options.template;
this.data = options.data;
this.childViews = options.childViews;
this.el = document.createElement("div");
}
MainView.prototype.render = function() {
View.prototype.render.call(this);
this.postRender();
return this;
};
MainView.prototype.postRender = function() {
this.el.getElementsByTagName("i")[0].style.border = "1px dotted red";
};
function View(options) {
this.template = options.template;
this.data = options.data;
this.childViews = options.childViews;
this.el = document.createElement("div");
}
View.prototype.render = function(renderChildren) {
if(this.childViews) {
this.data.outlet = "<div class=\"outlet\"></div>";
}
this.el.innerHTML = this.template(this.data);
if(this.childViews) {
this.childViews.forEach(function(view) {
this.el.getElementsByClassName("outlet")[0].appendChild(view.render().el);
}, this);
}
return this;
};
function traverse(node) {
var viewOptions = {
template: templates[node.template],
data: node.data
};
if(node.children) {
viewOptions.childViews = node.children.map(function(n) {
return traverse(n);
});
}
return node.view ? new window[node.view + "View"](viewOptions) : new View(viewOptions);
}
function init() {
tree.forEach(function(node) {
var view = traverse(node);
document.body.appendChild(view.render().el);
});
}
window.onload = init;
Example here: http://jsfiddle.net/b4fTB/
The reason that I have the data in a tree is because the nested templates varies depending on the users data, and because many templates may be wrapped in different templates.
I don't know if what I'm doing here is stupid, but it allows me to render the templates from C# as well, which is quite nice.
So - the question (comments to the above is of course welcome). When dealing with a template with nested templates, it would be nice to have a simple function that only returns dom elements related to the actual template - and not dom elements from the nested templates. Is this even possible? And is it possible in a way that allows for deeply nested templates and not lose a great deal of performance? In other words, I have two templates where one of them is nested within the other in the jsfiddle. It would be nice not having to worry about nested views in the parent view when dealing with the dom.
Alright, I think I've found a way myself.
Following code requires mustachejs and composejs:
var noop = function() {};
var templates = {
secondTmpl: Mustache.compile("<i>Yet another template.. {{{outlet}}}Here's some text: {{text}}</i> {{date}}"),
template: Mustache.compile("<b>{{val}}</b><p>{{{outlet}}}</p><ul>{{#list}}<li>{{.}}</li>{{/list}}</ul> {{test}}")
};
var tree = [
{
view: "Main",
template: "template",
data: { val: "yup!!", list: [1, 2, 3, "Four", 5], test: function() { return new Date(); } },
children: [
{
template: "secondTmpl",
data: { text: "Some Value", date: function() { return new Date(); } }
},
{
view: "Test",
template: "secondTmpl",
data: { text: "ANOTHER TEMPLATE", date: function() { return new Date(); } },
children: [
{
template: "template",
data: { val: "Pretty nested template", list: [56, 52, 233, "afsdf", 785], test: "no datetime here" }
}
]
}
]
}
];
var View = Compose(function(options) {
Compose.call(this, options);
this.el = document.createElement(this.tag);
}, {
tag: "div",
render: function() {
if(this.childViews) {
this.data.outlet = "<div class=\"outlet\"></div>";
}
this.el.innerHTML = this.template(this.data);
this.didRender();
if(this.childViews) {
var lastEl;
this.childViews.forEach(function(view) {
if(!lastEl) {
var outlet = this.el.getElementsByClassName("outlet")[0];
lastEl = view.render().el;
outlet.parentNode.replaceChild(lastEl, outlet);
} else {
var el = view.render().el;
lastEl.parentNode.insertBefore(el, lastEl.nextSibling);
lastEl = el;
}
}, this);
}
this.didRenderDescendants();
return this;
},
didRender: noop,
didRenderDescendants: noop
});
var TestView = View.extend({
didRender: function() {
var nodes = this.el.querySelectorAll("*");
for(var i = 0; i < nodes.length;i++)
nodes[i].style.border = "2px dotted red";
}
});
var MainView = View.extend({
didRender: function() {
var nodes = this.el.querySelectorAll("*");
for(var i = 0; i < nodes.length;i++)
nodes[i].style.backgroundColor = "lightgray";
}
});
function traverse(node) {
var viewOptions = {
template: templates[node.template],
data: node.data
};
if(node.children) {
viewOptions.childViews = node.children.map(function(n) {
return traverse(n);
});
}
return node.view ? new window[node.view + "View"](viewOptions) : new View(viewOptions);
}
function init() {
tree.forEach(function(node) {
var view = traverse(node);
window["view"] = view;
document.body.appendChild(view.render().el);
});
}
window.onload = init;
The trick is to replace the div.outlet with the first child view instead of appending to it. Then it's a matter of inserting the other child views next to each other.
Related
I have a tree view with parent node and child node. Now, I am able to add the parent node under sub nodes and child node under sub nodes of child's.
How to save added child nodes and parent nodes locally using java script?
javascript:
<script type="text/javascript">
onload = function() {
// create the tree
var theTree = new wijmo.nav.TreeView('#theTree', {
itemsSource: getData(),
displayMemberPath: 'header',
childItemsPath: 'items'
});
theTree.selectedItem = theTree.itemsSource[0];
// handle buttons
document.getElementById('btnFirst').addEventListener('click', function () {
var newItem = { header: document.getElementById('theInput').value },
node = theTree.selectedNode;
if (node) {
theTree.selectedNode = node.addChildNode(0, newItem);
} else {
theTree.selectedNode = theTree.addChildNode(0, newItem);
}
});
document.getElementById('btnLast').addEventListener('click', function () {
var newItem = { header: document.getElementById('theInput').value },
node = theTree.selectedNode;
if (node) {
var index = node.nodes ? node.nodes.length : 0;
theTree.selectedNode = node.addChildNode(index, newItem);
} else {
var index = theTree.nodes ? theTree.nodes.length : 0;
theTree.selectedNode = theTree.addChildNode(index, newItem);
}
});
document.getElementById('btnNoSel').addEventListener('click', function () {
theTree.selectedNode = null;
});
// create some data
function getData() {
return [
{ header: 'Building', items: [
{ header: 'Floors' },
]
},
];
}
}
</script>
Save item html:
localStorage.setItem("my_saved_element", your_element.innerHTML)
Load item html:
var my_new_el = document.createElement(localStorage.getItem("my_saved_element"))
I'm trying to map a nested ViewModel (three levels of depth) with Knockout's mapping plugin. When running this code, only the first Level will be mapped correctly. What am I doing wrong here?
Thanks in advance
Here is my Code:
var mapping = {
create: function (options) {
var levelOneItems = new levelOneModel(options.data)
//Some computed observables for level one here...
return levelOneItems;
},
'levelTwoItemList': {
create: function (options) {
var levelTwoItems = new levelTwoModel(options.data)
//Some computed observables for level two here...
return levelTwoItems;
},
'levelThreeItemList': {
create: function (options) {
var levelThreeItems = new levelThreeModel(options.data)
//Some computed observables for level three here...
return levelThreeItems;
}
}
}
}
var levelOneModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var levelTwoModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var levelThreeModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var data = [
{
'LevelOneName': 'Apple1',
'levelTwoItemList': [
{
'LevelTwoName': 'Apple2.1',
'levelThreeItemList': [
{ 'LevelThreeItemName': 'Apple3.1' },
{ 'LevelThreeItemName': 'Apple3.2' }
]
}, {
'LevelTwoName': 'Apple2.2',
'levelThreeItemList': [
{ 'LevelThreeItemName': 'Apple3.3' },
{ 'LevelThreeItemName': 'Apple3.4' }
]
},
]
}
]
var viewModel = ko.mapping.fromJS(data, mapping);
I've just figured it out myself while playing around with this objects. I hope this helps someone who got into the same trouble.
Here the code:
var mapping1 = {
create: function (options) {
var levelOneItems = ko.mapping.fromJS(options.data, mapping2)
//Some computed observables for level one here...
return levelOneItems;
}
}
var mapping2 = {
'levelTwoItemList': {
create: function (options) {
var levelTwoItems = ko.mapping.fromJS(options.data, mapping3)
//Some computed observables for level two here...
return levelTwoItems;
}
}
}
var mapping3 = {
'levelThreeItemList': {
create: function (options) {
var levelThreeItems = new levelThreeModel(options.data)
//Some computed observables for level three here...
return levelThreeItems;
}
}
}
var levelOneModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var levelTwoModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var levelThreeModel = function (data) {
ko.mapping.fromJS(data, {}, this);
}
var data = [
{
'LevelOneName': 'Apple1',
'levelTwoItemList': [
{
'LevelTwoName': 'Apple2.1',
'levelThreeItemList': [
{ 'LevelThreeItemName': 'Apple3.1' },
{ 'LevelThreeItemName': 'Apple3.2' }
]
}, {
'LevelTwoName': 'Apple2.2',
'levelThreeItemList': [
{ 'LevelThreeItemName': 'Apple3.3' },
{ 'LevelThreeItemName': 'Apple3.4' }
]
},
]
}
]
var viewModel = ko.mapping.fromJS(data, mapping1)
I'm starting to learn and azure phonejs.
Todo list get through a standard example:
$(function() {
var client = new WindowsAzure.MobileServiceClient('https://zaburrito.azure-mobile.net/', 'key');
var todoItemTable = client.getTable('todoitem');
// Read current data and rebuild UI.
// If you plan to generate complex UIs like this, consider using a JavaScript templating library.
function refreshTodoItems() {
var query = todoItemTable.where({ complete: false });
query.read().then(function(todoItems) {
var listItems = $.map(todoItems, function(item) {
return $('<li>')
.attr('data-todoitem-id', item.id)
.append($('<button class="item-delete">Delete</button>'))
.append($('<input type="checkbox" class="item-complete">').prop('checked', item.complete))
.append($('<div>').append($('<input class="item-text">').val(item.text)));
});
$('#todo-items').empty().append(listItems).toggle(listItems.length > 0);
$('#summary').html('<strong>' + todoItems.length + '</strong> item(s)');
}, handleError);
}
function handleError(error) {
var text = error + (error.request ? ' - ' + error.request.status : '');
$('#errorlog').append($('<li>').text(text));
}
function getTodoItemId(formElement) {
return $(formElement).closest('li').attr('data-todoitem-id');
}
// Handle insert
$('#add-item').submit(function(evt) {
var textbox = $('#new-item-text'),
itemText = textbox.val();
if (itemText !== '') {
todoItemTable.insert({ text: itemText, complete: false }).then(refreshTodoItems, handleError);
}
textbox.val('').focus();
evt.preventDefault();
});
// Handle update
$(document.body).on('change', '.item-text', function() {
var newText = $(this).val();
todoItemTable.update({ id: getTodoItemId(this), text: newText }).then(null, handleError);
});
$(document.body).on('change', '.item-complete', function() {
var isComplete = $(this).prop('checked');
todoItemTable.update({ id: getTodoItemId(this), complete: isComplete }).then(refreshTodoItems, handleError);
});
// Handle delete
$(document.body).on('click', '.item-delete', function () {
todoItemTable.del({ id: getTodoItemId(this) }).then(refreshTodoItems, handleError);
});
// On initial load, start by fetching the current data
refreshTodoItems();
});
and it works!
Changed for the use of phonejs and the program stops working, even mistakes does not issue!
This my View:
<div data-options="dxView : { name: 'home', title: 'Home' } " >
<div class="home-view" data-options="dxContent : { targetPlaceholder: 'content' } " >
<button data-bind="click: incrementClickCounter">Click me</button>
<span data-bind="text: listData"></span>
<div data-bind="dxList:{
dataSource: listData,
itemTemplate:'toDoItemTemplate'}">
<div data-options="dxTemplate:{ name:'toDoItemTemplate' }">
<div style="float:left; width:100%;">
<h1 data-bind="text: name"></h1>
</div>
</div>
</div>
</div>
This my ViewModel:
Application1.home = function (params) {
var client = new WindowsAzure.MobileServiceClient('https://zaburrito.azure-mobile.net/', 'key');
var todoItemTable = client.getTable('todoitem');
var toDoArray = ko.observableArray([
{ name: "111", type: "111" },
{ name: "222", type: "222" }]);
var query = todoItemTable.where({ complete: false });
query.read().then(function (todoItems) {
for (var i = 0; i < todoItems.length; i++) {
toDoArray.push({ name: todoItems[i].text, type: "NEW!" });
}
});
var viewModel = {
listData: toDoArray,
incrementClickCounter: function () {
todoItemTable = client.getTable('todoitem');
toDoArray.push({ name: "Zippy", type: "Unknown" });
}
};
return viewModel;
};
I can easily add items to the list of programs, but from the server list does not come:-(
I am driven to exhaustion and can not solve the problem for 3 days, which is critical for me!
Specify where my mistake! Thank U!
I suggest you use a DevExpress.data.DataSource and a DevExpress.data.CustomStore instead of ko.observableArray.
Application1.home = function (params) {
var client = new WindowsAzure.MobileServiceClient('https://zaburrito.azure-mobile.net/', 'key');
var todoItemTable = client.getTable('todoitem');
var toDoArray = [];
var store = new DevExpress.data.CustomStore({
load: function(loadOptions) {
var d = $.Deferred();
if(toDoArray.length) {
d.resolve(toDoArray);
} else {
todoItemTable
.where({ complete: false })
.read()
.then(function(todoItems) {
for (var i = 0; i < todoItems.length; i++) {
toDoArray.push({ name: todoItems[i].text, type: "NEW!" });
}
d.resolve(toDoArray);
});
}
return d.promise();
},
insert: function(values) {
return toDoArray.push(values) - 1;
},
remove: function(key) {
if (!(key in toDoArray))
throw Error("Unknown key");
toDoArray.splice(key, 1);
},
update: function(key, values) {
if (!(key in toDoArray))
throw Error("Unknown key");
toDoArray[key] = $.extend(true, toDoArray[key], values);
}
});
var source = new DevExpress.data.DataSource(store);
// older version
store.modified.add(function() { source.load(); });
// starting from 14.2:
// store.on("modified", function() { source.load(); });
var viewModel = {
listData: source,
incrementClickCounter: function () {
store.insert({ name: "Zippy", type: "Unknown" });
}
};
return viewModel;
}
You can read more about it here and here.
I am looking to run the following controller but im having trouble with scope.
I have a service that calls two functions that retrieve meta data to populate scope variables.
The issue is that using the service to call back the data interferes with other actions happening in the code. I have a directive on a tag that shows/hides an error on the span element once the rule is validated. This is now not functioning correctly. I run the code without asynchronous functions then everything works correctly.
My Plunker code is here
and the plunker of the desired behaviour is here
Plunker working example without dynamic data loading
<form class="form-horizontal">
<div class="control-group" ng-repeat="field in viewModel.Fields">
<label class="control-label">{{field.label}}</label>
<div class="controls">
<input type="text" id="{{field.Name}}" ng-model="field.data" validator="viewModel.validator" ruleSetName="{{field.ruleSet}}"/>
<span validation-Message-For="{{field.Name}}"></span>
</div>
</div>
<button ng-click="save()">Submit</button>
</form>
How do I get all bindings to update so everything is sync and loaded correctly?
angular.module('dataApp', ['servicesModule', 'directivesModule'])
.controller('dataCtrl', ['$scope', 'ProcessService', 'ValidationRuleFactory', 'Validator',
function($scope, ValidationRuleFactory, Validator, ProcessService) {
$scope.viewModel = {};
var FormFields = {};
// we would get this from the meta api
ProcessService.getProcessMetaData().then(function(data) {
alert("here");
FormFields = {
Name: "Course",
Fields: [{
type: "text",
Name: "name",
label: "Name",
data: "",
required: true,
ruleSet: "personFirstNameRules"
}, {
type: "text",
Name: "description",
label: "Description",
data: "",
required: true,
ruleSet: "personEmailRules"
}]
};
$scope.viewModel.Fields = FormFields;
ProcessService.getProcessRuleData().then(function(data) {
var genericErrorMessages = {
required: 'Required',
minlength: 'value length must be at least %s characters',
maxlength: 'value length must be less than %s characters'
};
var rules = new ValidationRuleFactory(genericErrorMessages);
$scope.viewModel.validationRules = {
personFirstNameRules: [rules.isRequired(), rules.minLength(3)],
personEmailRules: [rules.isRequired(), rules.minLength(3), rules.maxLength(7)]
};
$scope.viewModel.validator = new Validator($scope.viewModel.validationRules);
});
});
var getRuleSetValuesMap = function() {
return {
personFirstNameRules: $scope.viewModel.Fields[0].data,
personEmailRules: $scope.viewModel.Fields[1].data
};
};
$scope.save = function() {
$scope.viewModel.validator.validateAllRules(getRuleSetValuesMap());
if ($scope.viewModel.validator.hasErrors()) {
$scope.viewModel.validator.triggerValidationChanged();
return;
} else {
alert('person saved in!');
}
};
}
]);
The validation message directive is here
(function(angular, $) {
angular.module('directivesModule')
.directive('validationMessageFor', [function() {
return {
restrict: 'A',
scope: {eID: '#val'},
link: function(scope, element, attributes) {
//var errorElementId = attributes.validationMessageFor;
attributes.$observe('validationMessageFor', function(value) {
errorElementId = value;
//alert("called");
if (!errorElementId) {
return;
}
var areCustomErrorsWatched = false;
var watchRuleChange = function(validationInfo, rule) {
scope.$watch(function() {
return validationInfo.validator.ruleSetHasErrors(validationInfo.ruleSetName, rule.errorCode);
}, showErrorInfoIfNeeded);
};
var watchCustomErrors = function(validationInfo) {
if (!areCustomErrorsWatched && validationInfo && validationInfo.validator) {
areCustomErrorsWatched = true;
var validator = validationInfo.validator;
var rules = validator.validationRules[validationInfo.ruleSetName];
for (var i = 0; i < rules.length; i++) {
watchRuleChange(validationInfo, rules[i]);
}
}
};
// get element for which we are showing error information by id
var errorElement = $("#" + errorElementId);
var errorElementController = angular.element(errorElement).controller('ngModel');
var validatorsController = angular.element(errorElement).controller('validator');
var getValidationInfo = function() {
return validatorsController && validatorsController.validationInfoIsDefined() ? validatorsController.validationInfo : null;
};
var validationChanged = false;
var subscribeToValidationChanged = function() {
if (validatorsController.validationInfoIsDefined()) {
validatorsController.validationInfo.validator.watchValidationChanged(function() {
validationChanged = true;
showErrorInfoIfNeeded();
});
// setup a watch on rule errors if it's not already set
watchCustomErrors(validatorsController.validationInfo);
}
};
var getErrorMessage = function(value) {
var validationInfo = getValidationInfo();
if (!validationInfo) {
return '';
}
var errorMessage = "";
var errors = validationInfo.validator.errors[validationInfo.ruleSetName];
var rules = validationInfo.validator.validationRules[validationInfo.ruleSetName];
for (var errorCode in errors) {
if (errors[errorCode]) {
var errorCodeRule = _.findWhere(rules, {errorCode: errorCode});
if (errorCodeRule) {
errorMessage += errorCodeRule.validate(value).errorMessage;
break;
}
}
}
return errorMessage;
};
var showErrorInfoIfNeeded = function() {
var validationInfo = getValidationInfo();
if (!validationInfo) {
return;
}
var needsAttention = validatorsController.ruleSetHasErrors() && (errorElementController && errorElementController.$dirty || validationChanged);
if (needsAttention) {
// compose and show error message
var errorMessage = getErrorMessage(element.val());
// set and show error message
element.text(errorMessage);
element.show();
} else {
element.hide();
}
};
subscribeToValidationChanged();
if (errorElementController)
{
scope.$watch(function() {
return errorElementController.$dirty;
}, showErrorInfoIfNeeded);
}
scope.$watch(function() {
return validatorsController.validationInfoIsDefined();
}, subscribeToValidationChanged());
});
}
};
}]);
})(angular, $);
I know Im pretty close to figuring this out. Im trying to filter out my collection based on if favorite eq true. If I console.log - I can see it's doing its job. But it's not updating my view.
Anyone have any idea what I'm missing or doing wrong?
Here is my code:
var Products = Backbone.Model.extend({
// Set default values.
defaults: {
favorite: false
}
});
var ProductListCollection = Backbone.Collection.extend({
model: Products,
url: '/js/data/wine_list.json',
parse: function(data) {
return data;
},
comparator: function(products) {
return products.get('Vintage');
},
favoritesFilter1: function(favorite) {
return this.filter(function(products) {
return products.get('favorite') == true;
});
},
favoritesFilter: function() {
return this.filter(function(products) {
return products.get('favorite') == true;
});
},
});
var products = new ProductListCollection();
var ProductListItemView = Backbone.View.extend({
el: '#wine-cellar-list',
initialize: function() {
products.bind('reset', this.render, this);
products.fetch();
this.render();
},
render: function() {
console.log(this.collection);
var source = $('#product-template').html();
var template = Handlebars.compile(source);
var html = template(this.collection.toJSON());
this.$el.html(html);
return this;
},
});
// Create instances of the views
var productView = new ProductListItemView({
collection: products
});
var CellarRouter = Backbone.Router.extend({
routes: {
'': 'default',
"favorites": "showFavorites",
"purchased": "showPurchased",
"top-rated": "showTopRated",
},
default: function() {
productView.render();
},
showFavorites: function() {
console.log('Favorites');
productView.initialize(products.favoritesFilter());
},
showPurchased: function() {
console.log('Purchased');
},
showTopRated: function() {
console.log('Top Rated');
}
});
$(function() {
var myCellarRouter = new CellarRouter();
Backbone.history.start();
});
There's many mistakes in your code, I'll try to clarify the most I can :
Your collection should be just like this :
var ProductListCollection = Backbone.Collection.extend({
model: Products,
url: '/js/data/wine_list.json',
comparator: 'Vintage' // I guess you want to sort by this field
});
Your view like this :
var ProductListItemView = Backbone.View.extend({
el: '#wine-cellar-list',
initialize: function() {
this.collection.bind('reset', this.full, this);
this.collection.fetch();
},
full: function() {
this.render(this.collection.models);
},
favorites: function(favorite) {
this.render(this.collection.where(favorite)); // here's the answer to your question
},
render: function(models) {
console.log(models);
var source = $('#product-template').html();
var template = Handlebars.compile(source);
var html = template(models.toJSON()); // You may have to change this line
this.$el.html(html);
return this;
},
});
And in your router :
showFavorites: function() {
console.log('Favorites');
productView.favorites(true); // or false, as you like
}