I have the following javascript:
function RandomViewModel() {
var self = this;
self.RnadomSquaresK = ko.observableArray([
randomSquare(),
randomSquare(),
]);
}
var randomSquare = function() { return ko.observable({
innate: ko.observableArray([
{ star: "test1", Class: "starList", Style: {} },
{ star: "test2", Class: "starList", Style: {display:'inline'} },
{ star: "test3", Class: "starList", Style: {} },
{ star: "test4", Class: "starList", Style: {} }
])
})};
ko.applyBindings(new RandomViewModel());
It's basically creating an observableArray of two observableArrays each with 4 elements.
I have the following html:
<script id="starTemplate" type="text/html">
<!-- ko if: ($index >= 0) -->
<div data-bind="text: $index, attr: { class: Class }, style: Style"></div>
<!-- /ko -->
</script>
<div class="starList" data-bind="template: { name: 'starTemplate', foreach: RnadomSquaresK()[0]().innate }"></div>
I'm expecting divs to be created with each of their indexes printed according to the observableArray they're bound to. (in this case: 4 divs, since all indexes should be equal or larger than 0)
What I actually get: blank.
Here's the JSFiddle link: http://jsfiddle.net/qrwBE/
If I change the if statement to ex: if: $index != 0, I'd print all 4 elements bound to the array, but I don't quite understand why in this case the first element (index 0) is being printed alongside the other 3 elements.
Am I using the if statement incorrectly?
Explanations on what's happening, and any other comments regarding javascript, etc. are much welcome.
So there are a number of items to examine here:
Why isn't your foreach loop displaying divs with index values in them?
The answer is because you $index() is an observable, which are functions. So you must use it like I demonstrated above. The reason why if: $index != 0 worked is because in javascript, things that evaluate to 0 are false any non-zero evaluates to true (just like in c/c++ if you are familiar with those languages.
So this means when you wrote if: $index != 0 you where really saying, is the function $index not null or undefined? which was true so it would continue to output divs from your template.
Refactor Recommendation:
A foreach binding will automatically take care of not rendering the div tags in the event that your observableArray is empty. This means you can just remove the if check altogether ... which also would have fixed your issue.
<script id="starTemplate"a type="text/html">
<div data-bind="text: $index, attr: { class: Class }, style: Style"></div>
</script>
Model changes:
Let's examine this function:
var randomSquare = function() { return ko.observable({
innate: ko.observableArray([
{ star: "test1", Class: "starList", Style: {} },
{ star: "test2", Class: "starList", Style: {display:'inline'} },
{ star: "test3", Class: "starList", Style: {} },
{ star: "test4", Class: "starList", Style: {} }
])
})};
Here you are returning an observable that contains a property called innate which is an observable array. Why not just remove this being contained in a observable because it only makes the syntax for accessing the array more funky RnadomSquaresK()[0]().innate
Instead of that, how about the following:
var randomSquare = function() { return {
innate: ko.observableArray([
{ star: "test1", Class: "starList", Style: {} },
{ star: "test2", Class: "starList", Style: {display:'inline'} },
{ star: "test3", Class: "starList", Style: {} },
{ star: "test4", Class: "starList", Style: {} }
]});
})};
which would simply accessing it to the following: RnadomSquaresK()[0].innate
When to make something Observable
Just remember that you only ever need to make something an observable ... if you intend for the UI or a subscriber function in js to be alerted when a change in the observable's state occurs. Otherwise, just make it a copied value (plain old js variable).
The dangers of mixing function declarations with function expressions (Hoisting concerns):
This is purely a js note.
This is a function declaration function RandomViewModel() { }
This is a function expression var randomSquare = function() { };
These are slightly different in how they are handled by the interpretters. The first one (declaration) will be hoisted to the top of it's parent scope. Therefore in subsequent code calling RandomViewModel() will work flawlessly because it is defined before everything else thanks to the interpreter.
However, the second one (expression) will only have the variable name hoisted to the top, but it's assignment will stay where it is, which means your code would be equivalent to this:
function RandomViewModel() { }
var randomSquare;
randomSquare = function() { };
This will work fine in your case. However, as your models become more complex and more tightly coupled with one another, you may end up with issues like this scenario:
function RandomViewModel() {
var x = randomSquare(); //<-- would throw an undefined exception here
alert(x);
}
var model = new RandomViewModel();
var randomSquare = function() { return 5; };
This would happen because the interpretter hoisted your var randomSquare to the top of the scope but it isn't assigned a function until after it's attemped use in RandomViewModel
Safe bet, do it a consistent, don't mix and match these to appraoches to writing functions.
When doing javascript expressions in a binding, you need to get the value of the property:
http://jsfiddle.net/wiredprairie/EWrsF/
if: $index() > 0
In use:
<script id="starTemplate" type="text/html">
<!-- ko if: $index() > 0 -->
<div data-bind="text: star, attr: { class: Class }, style: Style"></div>
<!-- /ko -->
</script>
I admittedly don't understand the rest of your code (as the variable names and pattern you're using is challenging to understand).
Related
I am trying to understand how I can bind data from the view-model to the view. The REST request to the back-end is working fine and I get a JSON array with several items. The existing documentation doesn't give me enough help.
How can I bind the timeline component ojtimeline to the view-model data array?
Edit: No errors now, since the view recognize the view-model array. But the ojtimeline doesn't display the data, only a working empty view component.
View
<div id="tline"
data-bind='ojComponent: {
component: "ojTimeline",
minorAxis: {
scale: "hours",
zoomOrder: ["hours", "days", "weeks"]
},
majorAxis: {
scale: "weeks"
},
start: new Date("Jan 1, 2016").toISOString(),
end: new Date("Jun 31, 2016").toISOString(),
referenceObjects: [{value: new Date("Feb 1, 2010").toISOString()}],
series: [{
id: "id",
emptyText: "No Data.",
items: statusArray,
label: "Oracle Events"
}],
overview: {
rendered: "off"
}
}' style="width: '100%';height: 350px"></div>
View-model
define(['ojs/ojcore', 'knockout', 'jquery', 'ojs/ojknockout', 'ojs/ojtimeline'],
function (oj, ko) {
/**
* The view model for the main content view template
*/
function timelineContentViewModel() {
var self = this;
this.statusArray = ko.observableArray([]);
self.addData = function () {
$.ajax({
url: "http://localhost:8080/myproject/rest/status/v1/findAll",
type: 'GET',
dataType: 'json',
success: function (data, textStatus, jqXHR) {
var x = data;
for (i = 0; i < x.length; i++) {
statusArray.push({
id: data[i].id,
description: data[i].text,
title: data[i].user.screenName,
start: data[i].createdAt});
}
//$("#tline").ojTimeline("refresh"); Doesn't have ant affect
}
});
};
self.addData();
}
return timelineContentViewModel;
});
The ReferenceError is caused by
var statusArray = ko.observableArray([]);
it should be
this.statusArray = ko.observableArray([])
You will also (probably) need to refresh the timeline when the observable array has changed, e.g. after the for-loop in success callback:
...
success: function (data, textStatus, jqXHR) {
var x = data;
for (i = 0; i < x.length; i++) {
self.statusArray.push({
id: data[i].id,
description: data[i].text,
title: data[i].user.screenName,
start: data[i].createdAt});
}
$("#tline").ojTimeline("refresh");
}
...
I have loaded ojTimeline from Ajax data and have never needed to use refresh. Worst case, you can wrap the ojTimeline in a <!-- ko if ... --> so that the timeline doesn't appear until you have an Ajax response.
For the ojTimeline items attribute, instead of referencing the observable, I had to unwrap the observable like this: items: ko.toJS(statusArray).
Another thing to consider is pushing into an ko.observableArray inside a for loop. Each push using the ko.observableArray push() method invokes subscriptions. If your array is bound to the UI, then each push will trigger a DOM change. Instead, it is often better to push into the underlying array (unwrap the array) and then invoke self.statusArray.valueHasMutated. You may also want to keep an eye on your use of this, self, and nothing. Consistency will help avoid bugs like the one ladar identified.
What do you think about rewriting your for loop like this (code untested)?
ko.utils.arrayPushAll(
self.statusArray(),
ko.utils.arrayMap(data, function(item) {
return {
id: item.id,
description: item.text,
title: item.user.screenName,
start: item.createdAt;
};
});
);
self.statusArray.valueHasMutated();
Or, if you can get away with it (some OJ components don't like this approach), you can skip the push and just replace the entire array inside the observable:
self.statusArray(
ko.utils.arrayMap(data, function(item) {
return {
id: item.id,
description: item.text,
title: item.user.screenName,
start: item.createdAt;
};
});
);
I'm using the video.js 4.12 library and I want replace control bar items. For example, move one of my custom buttons to the 2nd slot of the control bar.
How do I change the order of items on the taskbar? I had no luck on Google.
Videojs place good class on elements. By this way you can identify control bar's elements.
To handle the item's order I used Jquery :
var createPrevButton = function() {
var props = {
className: 'vjs-control player-prev-button', //We use this class in Jquery
innerHTML: '<div class="vjs-control-content"></div>',
role: 'button',
'aria-live': 'polite',
tabIndex: 0
};
return videojs.Component.prototype.createEl(null, props);
};
var myPlayer = me.player = videojs(me.idVideo, {
plugins : { chapters : {} },
children: {
controlBar: {
children: [
{
name: 'playToggle'
},
{
name: 'currentTimeDisplay'
},
{
name: 'timeDivider'
},
{
name: 'durationDisplay'
}
/*
...........
*/
]
}
}
});
$(".player-prev-button").insertAfter(".vjs-play-control");
$(".player-next-button").insertAfter(".player-prev-button");
After the instanciation of my player just handle item by Jquery.
I think it's better than use CSS.
But the best way should be by videojs's option or somethink like that
Hopefully this will be a more generic Angular JS question rather than something specific to Angular Formly.
I've been following the framework provided here for building an error summary on the angular formly form. All works well.....but!
In the example, their model is as follows:
vm.fields = [
{
key: 'picky',
type: 'customInput',
templateOptions: {
label: 'Picky field...',
placeholder: 'This is required and has a maxlength of 5 and minlength of 3',
required: true,
maxlength: 5,
minlength: 3
}
},
.....
{
key: 'ip',
type: 'customInput',
validators: {
ipAddress: {
expression: function(viewValue, modelValue) {
var value = modelValue || viewValue;
return /(\d{1,3}\.){3}\d{1,3}/.test(value);
},
message: '$viewValue + " is not a valid IP Address"'
}
},
templateOptions: {
label: 'IP Address',
required: true,
type: 'text',
placeholder: '127.0.0.1',
}
}
];
Then, if we look at the HTML, we can see that these fields are being passed into the error summary as such:
<formly-error-summary form="vm.form" fields="vm.fields"></formly-error-summary>
For simple form structure, this works fine, but, if you want to use a Bootstrap layout, as described here then your model ends up looking something mine does:
vm.rentalFields = [
{
template: '<div class="row"><div class="col-xs-12"><h3>About You</h3></div></div>'
},
{
className: 'row',
fieldGroup: [
{
className: 'col-xs-6',
type: 'customInput',
key: 'first_name',
templateOptions: {
type: 'text',
label: 'First Name',
placeholder: 'Enter your first name',
required: true
}
},
{
className: 'col-xs-6',
type: 'customInput',
key: 'last_name',
templateOptions: {
type: 'text',
label: 'Last Name',
placeholder: 'Enter your last name',
required: true
},
expressionProperties: {
'templateOptions.disabled': '!model.first_name'
}
}
]
},
{
template: '<div class="row"><div class="col-xs-12"><h3>License and Insurance Details</h3></div></div>',
hideExpression: '!model.email'
}
.....
Now, when we pass in vm.rentalFields to the error summary, instead of accessing the fields, it instead just validates each object. I can get round this by doing something like:
<formly-error-summary form="vm.rentalForm" fields="vm.rentalFields[1].fieldGroup"></formly-error-summary>
This of course is not ideal since there will be fields in other field groups that I will want to validate, for proving the issue though it's fine for now. I have tried just passing in 'vm.rentalFields.fieldGroup' but as I suspected, that returns nothing.
So, is there a way I can recursively pass in all the fieldGroups within the vm.rentalField object or is this something that I should handle within the code of the Directive itself.
angular.module("formlyApp").directive('formlyErrorSummary', function() {
return {
scope: {},
bindToController: {
form: '=',
fields: '='
},
templateUrl: 'js/Directives/formly-error-summary.html',
controllerAs: 'vm',
controller: function() {
var vm = this;
vm.getErrorAsList = getErrorAsList;
console.log(vm.fields);
function getErrorAsList(field) {
return Object.keys(field.formControl.$error).map(function(error) {
// note, this only works because the customInput type we have defined.
return field.data.getValidationMessage(error);
}).join(', ');
}
}
};
});
EDIT
Ok, so, after taking advice from Ken below, I have been able to modify my formlyErrorSummary directive so that it now is at least able to get the errors for the model. This has numerous issues in it since the $scope.$watch is doing a deep level comparison and even on the first page load, the whole thing is fired 3 times! I've added in some rudimentary escapes to try and combat this and for now at least I have the errors, the next issue I have is within the HTML where I am call ng-repeat="field in vm.fields" which is effectively the same issue, so how would I work round this? Part of me is thinking of some anonymous object that would hold the fields message and whether or not is is valid and then parse that inside the HTML, but I'm not sure if this way of thinking is applicable to Angular?
controller: function($scope) {
var vm = this;
$scope.$watch('vm.fields', function(){
for(var i = 0; i < vm.fields.length; i++)
if(vm.fields[i].fieldGroup) {
for(var j = 0; j < vm.fields[i].fieldGroup.length; j ++)
if(vm.fields[i].fieldGroup[j].formControl) {
var err = getErrorAsList(vm.fields[i].fieldGroup[j]);
if(err)
vm.getErrorAsList = err;
}
}
}, true);
SOLUTION - POSSIBLY
After much hacking around, I think I finally have this working so that the error messages are now displayed both inline and in a summary at the top.
My final directive function now creates an array each time it is run which will hold all of the error messages, it has to be flushed within the $watch otherwise when the field is valid, the error message will persist within the array so we simply rebuild the entire thing each time.....given I'm already using a deep level watch here, I'm hoping any performance hits will be negligible.
vm.errs = [];
$scope.$watch('vm.fields', function(){
vm.errs = [];
for(var i = 0; i < vm.fields.length; i++)
if(vm.fields[i].fieldGroup) {
for(var j = 0; j < vm.fields[i].fieldGroup.length; j ++)
if(vm.fields[i].fieldGroup[j].formControl) {
var err = getErrorAsList(vm.fields[i].fieldGroup[j]);
if(err)
if(vm.errs.indexOf(err) === -1)
vm.errs.push(err);
}
}
}, true);
Then, within the directives template, I had to remove the vm.fields reference as that was obviously not going to work in this approach. Since I knew that this summary would only be shown if the form was invlaid, I could remove so of the other checks that were being carried out and eventually ended up with the following HTML:
<div class="row">
<div class="col-xs-12">
<div class="formly-error-summary bg-danger" ng-if="vm.form.$invalid">
<div ng-repeat="err in vm.errs" class="color-error">
<i class="glyphicon glyphicon-remove"></i>
<span>
{{err}}
</span>
</div>
</div>
</div>
</div>
I'm still not 100% happy with this, it gets the job done, but I'm not sure if it's the 'Angular' way of doing this and the fact I'm using $scope.$watch on the fields object is a little bit annoying to my developer OCD, but a solution it is all the same.
If anyone has any refinements or suggestions for improvements to this let me know please, still getting to grips with Angular but this has been a pretty fun learning experience!
In my pursuit to get a binding for an associative array to work, I've made significant progress, but am still blocked by one particular problem.
I do not understand how to create a binding from strictly javascript
Here is a jsFiddle that shows more details than I have posted here:
jsFiddle
Basically, I want to do a new binding within the shown $.each function that would be equivalent to this...
<div data-template="display-associative-many" data-bind="repeat: Root.Items"></div>
Gets turned into this ...
<div data-template="display-associative-single" data-bind="source: Root['Items']['One']"></div>
<div data-template="display-associative-single" data-bind="source: Root['Items']['Two']"></div>
<div data-template="display-associative-single" data-bind="source: Root['Items']['Three']"></div>
And I am using the repeat binding to create that.
Since I cannot bind to an associative array, I just want to use a binding to write all of the bindings to the objects in it.
We start again with an associative array.
var input = {
"One" : { Name: "One", Id: "id/one" },
"Two" : { Name: "Two", Id: "id/two" },
"Three" : { Name: "Three", Id: "id/three" }
};
Now, we create a viewModel that will contain that associative array.
var viewModel = kendo.observable({
Name: "View Model",
Root: {
Items: input
}
});
kendo.bind('#example', viewModel);
Alarmingly, finding the items to bind was pretty easy, here is my binding so far;
$(function(){
kendo.data.binders.repeat = kendo.data.Binder.extend({
init: function(element, bindings, options) {
// detailed more in the jsFiddle
$.each(source, function (idx, elem) {
if (elem instanceof kendo.data.ObservableObject) {
// !---- THIS IS WHERE I AM HAVING TROUBLE -----! //
// we want to get a kendo template
var template = {};// ...... this would be $('#individual-item')
var result = {}; // perhaps the result of a template?
// now I need to basically "bind" "elem", which is
// basically source[key], as if it were a normal HTML binding
$(element).append(result); // "result" should be a binding, basically
}
});
// detailed more in the jsFiddle
},
refresh: function() {
// detailed more in the jsFiddle
},
change: function() {
// detailed more in the jsFiddle
}
});
});
I realize that I could just write out the HTML, but that would not perform the actual "binding" for kendo to track it.
I'm not really sure what you are attempting to do, but it seemed to me that the custom "repeat" binding was unnecessary. Here's what I came up with. Is this on track with what you are trying to do?
Here is a working jsFiddle example.
HTML
<div id="example">
<div data-template="display-associative-many" data-bind="source: Root.Items"></div>
</div>
<script type="text/x-kendo-template" id="display-associative-many">
#for (var prop in data) {#
# if (data.hasOwnProperty(prop)) {#
# if (data[prop].Id) {#
<div><span>${data[prop].Id}</span> : <span>${data[prop].Name}</span></div>
# }#
# }#
#}#
</script>
JavaScript
$(function () {
var input = {
"One" : { Name: "One", Id: "id/one" },
"Two" : { Name: "Two", Id: "id/two" },
"Three" : { Name: "Three", Id: "id/three" }
};
var viewModel = new kendo.data.ObservableObject({
Id: "test/id",
Root: {
Items: input
}
});
kendo.bind('#example', viewModel);
});
I got a custom Ext.Component with a view XTemplates. I do need some of theese Templates outside of the view in my controller too.
Is it possible to refer to static members in functions of a XTemplate. Or is there another much better way???
something like this:
Ext.define('app.view.ApplicationHeader', {
extend: 'Ext.Component',
name: 'app-header',
xtype: 'app-header',
height: 67,
margin: 0,
statics: {
mainIconTpl: new Ext.XTemplate('someTemplate'),
navigationItemsTpl: new Ext.XTemplate( 'anotherTemplate'),
userInfoTpl: new Ext.XTemplate('userTemplate')
},
html: new Ext.XTemplate('... {[ this.renderMainIcons() ]} {[ this.renderUserInfo() ]} ...',
'... {[ this.renderNavigationBarItems() ]} ...',
{
me: this,
renderMainIcons: function () {
return view.static.mainIconTpl.apply(MR.Sitemap.Items);
},
renderUserInfo: function () {
return view.static.userInfoTpl.apply();
},
renderNavigationBarItems: function () {
return view.static.navigationItemsTpl.apply();
}
}).apply()
});
i also dont know how i could apply subtemplates which are members of the view. I declared them global right know which i really dont like to do.
please!
Your code is not working because the apply method of the main template is called before the class definition (i.e. the define method) is even called.
You can create your static template that uses the other static members of the class in the post-create function (see the last param of the define method).
Then in order for the template to be available, I would override the initComponent method and set the html property there.
Ext.define('app.view.ApplicationHeader', {
extend: 'Ext.Component',
name: 'app-header',
xtype: 'app-header',
height: 67,
margin: 0,
statics: {
mainIconTpl: new Ext.XTemplate('someTemplate'),
navigationItemsTpl: new Ext.XTemplate('anotherTemplate'),
userInfoTpl: new Ext.XTemplate('userTemplate')
},
initComponent: function() {
// Here, your statics are available, and you're in the scope of your
// class *instance*
this.html = this.self.viewTemplate.apply();
this.callParent(arguments);
}
}, function() {
// In the post create function, this is the class constructor
// (i.e. app.view.ApplicationHeader)
var cls = this;
// In fact, you could also create your sub templates here if you prefer
// e.g.
// cls.useInfoTpl = new Ext.XTemplate('userTemplate')
// So, viewTemplate will be a static property of the class
cls.viewTemplate = new Ext.XTemplate('... {[ this.renderMainIcons() ]} {[ this.renderUserInfo() ]} ...',
'... {[ this.renderNavigationBarItems() ]} ...', {
renderMainIcons: function() {
return cls.mainIconTpl.apply();
},
renderUserInfo: function() {
return cls.userInfoTpl.apply();
},
renderNavigationBarItems: function() {
return cls.navigationItemsTpl.apply();
}
});
});
According to the link, you should be able to put this directly in your XTemplate. No need for statics
{[ MyApp.tpls.someOtherTpl.apply(values) ]}
Multiple templates in Nested List
You could also try putting all of these XTemplates in initComponent instead since you're not injecting any values for XTemplate after initial component render. The apply() will just return you an HTML fragment which should be able to be appended anywhere within the XTemplate.
If you're trying to put logical or conditional tpl operators i.e. <tpl for="parent.someVar">...</tpl> in any of the sub XTemplates, then that's another problem so it all depends on what you're trying to accomplish.
Ext.define('app.view.ApplicationHeader', {
extend: 'Ext.Component',
name: 'app-header',
xtype: 'app-header',
height: 67,
margin: 0,
initComponent: function() {
var me = this,
me.mainIconTpl = new Ext.XTemplate('someTemplate'),
me.navigationItemsTpl = new Ext.XTemplate( 'anotherTemplate'),
me.userInfoTpl = new Ext.XTemplate('userTemplate');
me.tpl = new Ext.XTemplate(
'...', me.mainIconTpl.apply(MR.Sitemap.Items),
'...', me.navigationItemsTpl.apply(someValues),
'...', me.userinfoTpl.apply(someValues),
'...'
);
Ext.apply(me, {
html: me.tpl
});
me.callParent();
}
});