I need a component that would;
Work within a web page to allow a nontechnical user to create and edit templates for customized reports.
Allow the user to easily place a set of pre-defined template data tokens in the report that conforms to the templating engine syntax without the user needing to know how to write the syntax themselves.
Support the ability to change and update data items that can be used in the report without extensive rewriting of the code or creating a new plugin.
The goal is for end users to create their own personalized templates for a specific set of JSON data. The ability to create templates would be available on a web page and the templates would be stored for later use. Storing and using the templates is not a problem, but an easy to use editor for the templates is.
I have been using jsRender for a templating engine and like it but I am open to other engines if what I need is easier to support.
My goal is to have an online editor, similar to MCE or CKEditor, that would have the ability to place a preset list of data tokens into a custom template the user creates. These templates would be used for auto-generated content based on the data supplied to the template engine.
The solution needs to be fairly simple on the user side. The user skills could range from "Not Ignorant" up to "Almost Programmer". I would like to avoid anything that involves the user actually having to learn to hand write HTML, understand jsRender or other template library syntax. A drop down list of insertable data would be ideal.
I don't see much need for any complex logic syntax. My need is basic data token replacement, not any complex logic.
You can use jsviews + inner jsrender.
Create custom tag:
$.views.tags({
render : {
isUpdate : false,
init : function (tagCtx, linkCtx) {
var tag = this;
tag._template = tagCtx.args[0];
tag._data = tagCtx.args[1];
tag.template = "<div class='content'></div>";
},
_render : function () {
var tag = this;
var template = $.templates(tag._template.text);
tag.linkedElem.html(template.render(tag._data));
},
onUpdateTemplate : function () {
this._render();
},
onUpdateData : function () {
this._render();
},
onAfterLink : function (tagCtx, linkCtx) {
var tag = this;
tag._template = tagCtx.args[0];
tag._data = tagCtx.args[1];
if (tag._.unlinked) {
if (!tag.linkedElem) {
tag.linkedElem = tag._.inline ? tag.contents("*").first() : $(linkCtx.elem);
}
$.observable(tag._template).observeAll($.proxy(tag.onUpdateTemplate, tag));
$.observable(tag._data).observeAll($.proxy(tag.onUpdateData, tag));
} else {
$.observable(tag._template).unobserveAll($.proxy(tag.onUpdateTemplate, tag));
$.observable(tag._data).unobserveAll($.proxy(tag.onUpdateData, tag));
$.observable(tag._template).observeAll($.proxy(tag.onUpdateTemplate, tag));
$.observable(tag._data).observeAll($.proxy(tag.onUpdateData, tag));
}
tag._render();
},
onDispose : function () {
debugger;
var tag = this;
$.observable(tag._template).unobserveAll($.proxy(tag.onUpdateTemplate, tag));
$.observable(tag._data).unobserveAll($.proxy(tag.onUpdateData, tag));
},
onUpdate : function () {
return false;
},
dataBoundOnly : true
}
});
Create html:
<div id="page"></div>
<script id="templateEditor" type="text/x-jsrender">
<select data-link="selectedTemplate">
<option value="-">Please select</option>
{^{for templates}}
<option data-link="value{:#index} {:name} selected{:#index == ~root.selectedTemplate}"></option>
{{/for}}
</select>
<button data-link="{on addNew}">Add</button>
<br/>
<br/>
{^{if selectedTemplate!=="-"}}
template Name:<input data-link="templates[selectedTemplate].name" />
<br/>
<br/>
<textarea name="template" cols="40" rows="5" data-link="templates[selectedTemplate].text trigger=true"></textarea>
{{/if}}
<br/>
<br/>
{^{if selectedTemplate!=="-"}}
{^{render templates[selectedTemplate] data/}}
{{/if}}
</script>
And add app code:
var app = {
data : {
value : "test"
},
templates : [{
name : "Main",
text : "<span>{{:value}}</span>"
}
],
selectedTemplate : 0,
addNew : function () {
$.observable(this.templates).insert({
name : "new",
text : ""
});
}
};
$([app.templates]).on("arrayChange", function (o, e) {
if (e.change === "insert") {
$.observable(app).setProperty("selectedTemplate", e.index);
}
});
var templateEditor = $.templates("#templateEditor");
templateEditor.link("#page", app);
see live example on jsfiddle
Update 1:
I don't see much need for any complex logic syntax. My need is basic data token replacement, not any complex logic.
If you do not want to use jsRender use this code:
function template(str, data) {
return str.replace(/%(\w*)%/g, function (m, key) {
return data.hasOwnProperty(key) ? data[key] : "";
});
}
example on jsfiddle
Related
My question is somewhat similar to followin unanswered question. (Not sure though)
Sitecore 8 SPEAK: Getting an Error When calling a Method in JS File
I am using Sitecore8
On my page there is a button and on its click event I want to call add() of custom datasource component.
Layout:
JS Code for the Page:
define(["sitecore"], function (Sitecore) {
var JsonListPage = Sitecore.Definitions.App.extend({
initialized: function () {
alert('Inside Json PageList Init');
},
loadData: function () {
alert('Button clicked');
app.add();
}
});
return JsonListPage;
});
JS Code for the custom datasource component:
define(["sitecore"], function (Sitecore) {
var model = Sitecore.Definitions.Models.ControlModel.extend({
initialize: function (options) {
this._super();
this.set("json", null);
alert('Inside Jsondatasource Init');
},
add: function (data) {
var json = this.get("json");
if (json === null)
json = new Array();
// this is done because array.push changes the array to an object which then do no work on the SPEAK listcontrol.
var newArray = new Array(json.length + 1);
for (var i = json.length - 1; i >= 0; i--)
newArray[i + 1] = json[i];
newArray[0] = data;
this.set("json", newArray);
}
});
var view = Sitecore.Definitions.Views.ControlView.extend({
initialize: function (options) {
this._super();
this.model.set("json", null);
}
});
Sitecore.Factories.createComponent("JsonDatasource", model, view, ".x-sitecore-jsondatasource");
});
.cshtml for Custom component:
#using Sitecore.Mvc
#using Sitecore.Mvc.Presentation
#using Sitecore.Web.UI.Controls.Common.UserControls
#model RenderingModel
#{
var userControl = Html.Sitecore().Controls().GetUserControl(Model.Rendering);
userControl.Requires.Script("client", "JsonDatasource.js");
userControl.Class = "x-sitecore-jsondatasource";
userControl.Attributes["type"] = "text/x-sitecore-jsondatasource";
userControl.DataBind = "Json: json";
var htmlAttributes = userControl.HtmlAttributes;
}
<div #htmlAttributes>
am here again
</div>
When the page loads:
It shows alert from Custom components Init
Then shows alert from host page's Init
On button click it shows the alert and after that gives error on "app".
There is some bit which I am missing.. any help would be appreciated.. Please let me know if you need anymore inputs.
Thanks in advance!
app is only available in debug mode so id avoid using that, use "this" instead.
From your code example it appears that you are calling app.Add(), There is no Add function on your pageCode, this is what your code is doing. Instead you need to access your components's Add Method.
Instead to access events within your component you want to call the function like this:
this.ComponentID.Add();
I have an example of a custom SPEAK component here you can refer to for how to create the component. https://github.com/sobek1985/MikeRobbinsSPEAKRichTextEditor
From the code is seems your creating a JSON datasource, there is an example by Anders here http://laubplusco.net/creating-simple-sitecore-speak-json-datasource/
I'm trying to do something which must be really simple to accomplish in Ember.
I want to show a button in my template based on the boolean state of a property:
{{#if canFavoriteTag}}
{{d-button action="favoriteTag" label="tagging.favorite" icon="star-o" class="admin-tag favorite-tag"}}
{{else}}
{{d-button action="unFavoriteTag" label="tagging.unfavorite" icon="star-o" class="admin-tag favorite-tag tag-unfavorite"}}
{{/if}}
I have created a property called canFavoriteTag with a function which I want to return true or false to the template based on whether the user can favorite the tag or not:
export default Ember.Controller.extend(BulkTopicSelection, {
canFavoriteTag: function() {
const self = this;
var ticker = this.get('tag.id');
console.log('checking can fav stock:' + ticker);
Discourse.ajax("/stock/get_users_favorite_stocks", {
type: "GET",
}).then(function(data) {
var favable = true;
for (var i = data.stock.length - 1; i >= 0; i--) {
var stock = jQuery.parseJSON(data.stock[i]);
if(ticker.toLowerCase() == stock.symbol.toLowerCase()) { console.log(ticker + ' is a favorite stock: ' + stock.symbol.toLowerCase()); favable = false; }
}
console.log(favable);
return favable;
});
}.property('canFavoriteTag') <-- unsure about this?
...
When the page loads, the wrong button shows (always the "false" one).. I see in the console that the favable variable gets set to false when the ajax call completes, but the button never changes. How do I get it to show the right button based on the function? Do I need to use a promise? If so, how?
I'm having a difficult time returning data from a module using RequireJS and Knockout to populate my markup for my single page app. Knockout can't seem to find my data binding observables.
I'm trying to keep each view in a separate js file, but I'm failing to identify where I've gone wrong. Here's what I have so far:
/app/app.js
define(function(require) {
require('simrou');
var $ = require('jQuery'),
ko = require('knockout'),
videoView = require('videoView');
var init = function() {
var viewModel = function() {
var self = this;
self.currentPage = ko.observable();
self.videoView = new videoView();
}
var view = new viewModel();
ko.applyBindings( view );
_router = new Simrou({
'/video/:id': [ view.videoView.getVideo ]
});
_router.start();
};
return {
init: init
};
});
/app/videoView.js
define(function(require) {
"use strict";
var $ = require('jQuery'),
ko = require('knockout');
return function() {
var self = this;
self.currentPage = ko.observable( 'showVideo' );
self.currentVideo = ko.observable();
self.videoData = ko.observableArray([]);
self.videoList = ko.observableArray([]);
var getVideo = function( event, params ) {
// ajax pseudo code
$.ajax({});
self.videoData( dataFromAjaxCall );
}
return {
getVideo: getVideo
};
};
});
index.html
When I browse to /#/video/14 I receive the following error:
Uncaught ReferenceError: Unable to parse bindings.
Bindings value: attr: { 'data-video-id': videoData().id }
Message: videoData is not defined
Here's the markup:
<section id="showVideo" data-bind="fadeVisible: currentPage()=='showVideo', with: $root">
<div class="video" data-bind="attr: { 'data-video-id': videoData().id }></div>
</section>
Like I said, I'm trying to keep each view separated, but I would love some enlightenment on what I'm doing wrong, or if this is even possible? Is there a better more efficient way?
videoData is a property of $root.videoView, not of the root model (the one you passed to applyBindings). It's also an observableArray, so videoData() is just a plain array and even if you get the context right, you won't be able to access its id property, since, being an array, it doesn't have.named properties.
The problem
I am trying to test some directives (code for both below). One of them is an "email" (called "epost" in the code(norwegian)) directive. The solution to this should work for all of them, so I am keeping it to this one for now.
Technologies: Angularjs, Jasmine, Requirejs, (grunt & karma running in Chrome)
The directive validates email addresses in two ways; on upshift and on blur. I can test the upshift without problems as you can see in the test below, but I can't figure out how to simulate a blur so the bind('blur') in the directive runs.
What I have done
I have tried to catch the compiled element like this:
elem = angular.element(html);
element = $compile(elem)($scope);
And then in the test i tried several permutations to trigger the blur with a console log just inside the bind function in the directive. None of the below works. It does not trigger.
elem.trigger('blur');
element.trigger('blur');
elem.triggerHandler('blur');
element.triggerHandler('blur');
element.blur();
elem.blur();
I based the injection and setup on this: To test a custom validation angularjs directive
The email directive in angularjs wrapped in requirejs
define(function() {
var Directive = function() {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
var pattern = /^[A-Za-z0-9._%+-]+#[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
elem.bind('blur', function() {
scope.$apply(function () {
if (!elem.val() || pattern.test(elem.val())) {
ctrl.$setValidity('epost', true);
} else {
ctrl.$setValidity('epost', false);
}
});
});
ctrl.$parsers.unshift(function(viewValue) {
if (pattern.test(viewValue)) {
ctrl.$setValidity('epost', true);
return viewValue;
} else {
return undefined;
}
});
}
};
};
return Directive;
});
The test (using jasmine and requirejs)
define([
'Angular',
'AngularMocks',
], function () {
describe('Directives', function () {
var $scope;
var form;
beforeEach(module('common'));
beforeEach(function () {
var html = '<form name="form">';
html += '<input type="text" id="epost" name="epost" epost="" ng-model="model.epost"/>';
html += '</form>';
inject(function ($compile, $rootScope) {
$scope = $rootScope.$new();
$scope.model = {
epost: null
};
// Compile the element, run digest cycle
var elem = angular.element(html);
$compile(elem)($scope);
$scope.$digest();
form = $scope.form;
});
});
describe('(epost) Given an input field hooked up with the email directive', function () {
var validEmail = 'a#b.no';
var invalidEmail = 'asdf#asdf';
it('should bind data to model and be valid when email is valid on upshift', function () {
form.epost.$setViewValue(validEmail);
expect($scope.model.epost).toBe(validEmail);
expect(form.epost.$valid).toBe(true);
});
});
});
});
I have been able to figure out where I went wrong after some breakpoint debugging.
The "element" item I get out using the approach described in the top of the question is not actually the directive it self. It's an object which wraps the form and the directive.
Like this
{ 0: // The form
{ 0: // The directive (input element)
{
}
}
}
To actually simulate a blur on the directive it self, I did something like this
var directiveElement = $(element[0][0]);
directiveElement.blur();
After getting the element I wanted, and wrapping it in a jQuery object (may be optional), it worked like a charm. I then used the approach like in the test in the question with $setViewValue and checked the model value like this.
form.epost.$setViewValue('a#b.no');
directiveElement.blur();
expect($scope.model.epost).toBe('a#b.no');
expect($scope.form.epost.$valid).toBeTruthy();
Hope this could be of help to others trying to figure the directive testing out.
I too ran into a similar problem and it mystified me. My solution was to use JQuery to get the input and then use angular.element(input).triggerHandler('blur') to make it work. This is odd to me because I do not have to do this with the click event.
spyOn(controller, 'setRevenueIsInvalid');
var sugarRow = $(element).find('tr#ve_id_5')[0];
var amount = $(sugarRow).find('input.amount')[0];
angular.element(amount).triggerHandler('blur');
expect(controller.setRevenueIsInvalid).toHaveBeenCalled();
This code:
Html.CheckBoxList(ViewData.TemplateInfo.HtmlFieldPrefix, myList)
Produces this mark-up:
<ul><li><input name="Header.h_dist_cd" type="checkbox" value="BD" />
<span>BD - Dist BD Name</span></li>
<li><input name="Header.h_dist_cd" type="checkbox" value="SS" />
<span>SS - Dist SS Name</span></li>
<li><input name="Header.h_dist_cd" type="checkbox" value="DS" />
<span>DS - Dist DS Name</span></li>
<li><input name="Header.h_dist_cd" type="checkbox" value="SW" />
<span>SW - Dist SW Name </span></li>
</ul>
You can check multiple selections. The return string parameter Header.h_dist_cd only contains the first value selected. What do I need to do to get the other checked values?
The post method parameter looks like this:
public ActionResult Edit(Header header)
I'm assuming that Html.CheckBoxList is your extension and that's markup that you generated.
Based on what you're showing, two things to check:
The model binder is going to look for an object named Header with string property h_dist_cd to bind to. Your action method looks like Header is the root view model and not a child object of your model.
I don't know how you are handling the case where the checkboxes are cleared. The normal trick is to render a hidden field with the same name.
Also a nit, but you want to use 'label for="..."' so they can click the text to check/uncheck and for accessibility.
I've found that using extensions for this problem is error prone. You might want to consider a child view model instead. It fits in better with the EditorFor template system of MVC2.
Here's an example from our system...
In the view model, embed a reusable child model...
[AtLeastOneRequired(ErrorMessage = "(required)")]
public MultiSelectModel Cofamilies { get; set; }
You can initialize it with a standard list of SelectListItem...
MyViewModel(...)
{
List<SelectListItem> initialSelections = ...from controller or domain layer...;
Cofamilies = new MultiSelectModel(initialSelections);
...
The MultiSelectModel child model. Note the setter override on Value...
public class MultiSelectModel : ICountable
{
public MultiSelectModel(IEnumerable<SelectListItem> items)
{
Items = new List<SelectListItem>(items);
_value = new List<string>(Items.Count);
}
public int Count { get { return Items.Count(x => x.Selected); } }
public List<SelectListItem> Items { get; private set; }
private void _Select()
{
for (int i = 0; i < Items.Count; i++)
Items[i].Selected = Value[i] != "false";
}
public List<SelectListItem> SelectedItems
{
get { return Items.Where(x => x.Selected).ToList(); }
}
private void _SetSelectedValues(IEnumerable<string> values)
{
foreach (var item in Items)
{
var tmp = item;
item.Selected = values.Any(x => x == tmp.Value);
}
}
public List<string> SelectedValues
{
get { return SelectedItems.Select(x => x.Value).ToList(); }
set { _SetSelectedValues(value); }
}
public List<string> Value
{
get { return _value; }
set { _value = value; _Select(); }
}
private List<string> _value;
}
Now you can place your editor template in Views/Shared/MultiSelectModel.ascx...
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<WebUI.Cofamilies.Models.Shared.MultiSelectModel>" %>
<div class="set">
<%=Html.LabelFor(model => model)%>
<ul>
<% for (int i = 0; i < Model.Items.Count; i++)
{
var item = Model.Items[i];
string name = ViewData.ModelMetadata.PropertyName + ".Value[" + i + "]";
string id = ViewData.ModelMetadata.PropertyName + "_Value[" + i + "]";
string selected = item.Selected ? "checked=\"checked\"" : "";
%>
<li>
<input type="checkbox" name="<%= name %>" id="<%= id %>" <%= selected %> value="true" />
<label for="<%= id %>"><%= item.Text %></label>
<input type="hidden" name="<%= name %>" value="false" />
</li>
<% } %>
</ul>
<%= Html.ValidationMessageFor(model => model) %>
Two advantages to this approach:
You don't have to treat the list of items separate from the selection value. You can put attributes on the single property (e.g., AtLeastOneRequired is a custom attribute in our system)
you separate model and view (editor template). We have a horizontal and a vertical layout of checkboxes for example. You could also render "multiple selection" as two listboxes with back and forth buttons, multi-select list box, etc.
I think what you need is how gather selected values from CheckBoxList that user selected and here is my solution for that:
1- Download Jquery.json.js and add it to your view as reference:
2- I've added a ".cssMyClass" to all checkboxlist items so I grab the values by their css class:
<script type="text/javascript" >
$(document).ready(function () {
$("#btnSubmit").click(sendValues);
});
function populateValues()
{
var data = new Array();
$('.myCssClas').each(function () {
if ($(this).attr('checked')) {
var x = $(this).attr("value");
data.push(x);
}
});
return data;
}
function sendValues() {
var data = populateValues();
$.ajax({
type: 'POST',
url: '#Url.Content("~/Home/Save")',
data: $.json.encode(data),
dataType: 'json',
contentType: 'application/json; charset=utf-8',
success: function () { alert("1"); }
});
}
</script>
3- As you can see I've added all selected values to an Array and I've passed it to "Save" action of "Home" controller by ajax 4- in Controller you can receive the values by adding an array as argument:
[HttpPost]
public ActionResult Save(int[] val)
{
I've searched too much but apparently this is the only solution. Please let me know if you find a better solution for it.
when you have multiple items with the same name you will get their values separated with coma