I have prepared a basic fiddle of what I have here: http://jsfiddle.net/s103eqdc/
I have a function called relayButton, which loads and prepares initial data for view:
function relayButton(id, name, state, onChange) {
var self = this;
self.id = ko.observable(id);
self.name = ko.observable(name);
self.state = ko.observable(state);
self.state.subscribe(function(newValue) {
onChange(self, newValue);
});
}
But, how can I change the architecture of this simple code, so that, If there is a json data periodically loaded from server, it imidietly updates the proper relayId in the loop with checked or uncheked state?
You just need something to process the data when it comes from the backend and matches your relays id and updates the value.
I would do something like this
var app = window.app || {};
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function dataService() {
function refreshRelayData() {
return delay(200).then(function() {
return [{
id: '1',
name: 'relay1',
state: Math.round(Math.random())
},
{
id: '2',
name: 'relay2',
state: Math.round(Math.random())
},
{
id: '3',
name: 'relay3',
state: Math.round(Math.random())
}
];
});
}
return {
refreshRelayData:refreshRelayData
};
};
app.delay = delay;
app.dataService = dataService
function relayButton(id, name, state, onChange) {
var self = this;
self.id = ko.observable(id);
self.name = ko.observable(name);
self.state = ko.observable(state);
self.state.subscribe(function(newValue) {
onChange(self, newValue);
});
}
function ViewModel() {
var self = this;
self.availableRelays = ko.observableArray([]);
self.activeRelays = ko.computed(function() {
return self.availableRelays().filter(function(relay) {
return relay.state();
});
});
self.onRelayStateChange = function(item, newValue) {
console.log("State change event: " + item.name() + " (" + newValue + ")");
};
self.processData = function(data) {
data.forEach(function(item) {
self.availableRelays()
.filter(r => r.id() == item.id)
.forEach(r => r.state(item.state))
});
}
self.refreshData = function() {
app.dataService().refreshRelayData()
.then(data => self.processData(data));
}
self.init = function() {
self.availableRelays([
new relayButton(1, "relay1", 1, self.onRelayStateChange),
new relayButton(2, "relay2", 0, self.onRelayStateChange),
new relayButton(3, "relay3", 0, self.onRelayStateChange)
]);
};
}
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
viewModel.init();
setTimeout(function doSomething() {
viewModel.refreshData()
setTimeout(doSomething, 1000);
}, 1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div data-bind="foreach: $root.availableRelays">
<div class="switchBox">
<div class="switchName"><strong data-bind="text: ' ' + name()"></strong></div>
<div class="switchSlider">
<label class="relaySwitch">
<input class="relaySwitch-input" type="checkbox" data-bind="checked: state">
<span class="relaySwitch-label" data-on="On" data-off="Off"></span>
<span class="relaySwitch-handle"></span>
</label>
</div>
</div>
</div>
Related
This is my code
Viewmodel 1
function AppViewModel() {
var self = this;
self.boardtext = ko.observable();
self.board = ko.observableArray([
{ boardname: 'Board' },
{ boardname: 'Board' },
{ boardname: 'Board' }
]);
self.addboard = function () {
self.board.push({ boardname: self.boardtext() });
// initialize ListModal to zero
};
self.removeboard = function () {
self.board.remove(this);
}
}
Viewmodel 2
var initialData = [];
var ListModal = function (lists) {
var self = this;
self.cardtext = ko.observable();
self.lists = ko.observableArray(ko.utils.arrayMap(lists, function (list) {
return { listname: list.listname, cardlists: ko.observableArray(list.cardlists), showRenderTimes: ko.observable(false) };
}));
};
ko.applyBindings(new AppViewModel(), document.getElementById("container1"));
ko.applyBindings(new ListModal(initialData), document.getElementById("container2"));
As soon as i press addboard how can i set my ListModal to zero?
If you want to re-initialize listModal, try to wrap those 2 viewmodels in 1 viewmodel so that they can relate to each other. And then you can do the following:
var initialData = [];
var ListModal = function (lists) {
var self = this;
self.cardtext = ko.observable();
self.lists = ko.observableArray(ko.utils.arrayMap(lists, function (list) {
return { listname: list.listname, cardlists: ko.observableArray(list.cardlists), showRenderTimes: ko.observable(false) };
}));
}
function AppViewModel(parent) {
var self = this;
// this will keep the object of ViewModel
self.parentObject = parent;
self.boardtext = ko.observable();
self.board = ko.observableArray([
{ boardname: 'Board' },
{ boardname: 'Board' },
{ boardname: 'Board' }
]);
self.addboard = function () {
self.board.push({ boardname: self.boardtext() });
// re-initialize listModal
self.parentObject.listModal(new ListModal(initialData));
};
self.removeboard = function () {
self.board.remove(this);
};
}
function ViewModel() {
var self = this;
self.appViewModel = ko.observable(new AppViewModel(self));
self.listModal = ko.observable(new ListModal(initialData));
}
// provide another div which wrap container 1 and 2 together
ko.applyBindings(new ViewModel(), document.getElementById("container1And2"));
Trying to set creditCardExpMonth to the current month in the below Magento 2 JavaScript class (cc-form.js). The option month values are 1-12. When I manually add a month value like 3 creditCardExpMonth: 3, to the defaults:{ }, it works as expected. I just can't seem to figure out how to set it to the current month dynamically. I'm open to any solution that allows for the value to be overridden by the user's selection but I'd prefer it be inside this class or on the html page and not a JQuery update after the page loads.
I created a getCurrentMonth() function in this class but couldn't figure out how to access it correctly to set creditCardExpMonth to a default value.
define(
[
'underscore',
'Mageplaza_Osc/js/view/payment/default',
'Magento_Payment/js/model/credit-card-validation/credit-card-data',
'Magento_Payment/js/model/credit-card-validation/credit-card-number-validator',
'mage/translate'
],
function (_, Component, creditCardData, cardNumberValidator, $t) {
return Component.extend({
defaults: {
creditCardType: '',
creditCardExpYear: '',
creditCardExpMonth: '',
creditCardNumber: '',
creditCardSsStartMonth: '',
creditCardSsStartYear: '',
creditCardVerificationNumber: '',
selectedCardType: null
},
initObservable: function () {
this._super()
.observe([
'creditCardType',
'creditCardExpYear',
'creditCardExpMonth',
'creditCardNumber',
'creditCardVerificationNumber',
'creditCardSsStartMonth',
'creditCardSsStartYear',
'selectedCardType'
]);
return this;
},
initialize: function() {
var self = this;
this._super();
//Set credit card number to credit card data object
this.creditCardNumber.subscribe(function(value) {
var result;
self.selectedCardType(null);
if (value == '' || value == null) {
return false;
}
result = cardNumberValidator(value);
if (!result.isPotentiallyValid && !result.isValid) {
return false;
}
if (result.card !== null) {
self.selectedCardType(result.card.type);
creditCardData.creditCard = result.card;
}
if (result.isValid) {
creditCardData.creditCardNumber = value;
self.creditCardType(result.card.type);
}
});
//Set expiration year to credit card data object
this.creditCardExpYear.subscribe(function(value) {
creditCardData.expirationYear = value;
});
//Set expiration month to credit card data object
this.creditCardExpMonth.subscribe(function(value) {
creditCardData.expirationYear = value;
});
//Set cvv code to credit card data object
this.creditCardVerificationNumber.subscribe(function(value) {
creditCardData.cvvCode = value;
});
},
getCode: function() {
return 'cc';
},
getData: function() {
return {
'method': this.item.method,
'additional_data': {
'cc_cid': this.creditCardVerificationNumber(),
'cc_ss_start_month': this.creditCardSsStartMonth(),
'cc_ss_start_year': this.creditCardSsStartYear(),
'cc_type': this.creditCardType(),
'cc_exp_year': this.creditCardExpYear(),
'cc_exp_month': this.creditCardExpMonth(),
'cc_number': this.creditCardNumber()
}
};
},
getCcAvailableTypes: function() {
return window.checkoutConfig.payment.ccform.availableTypes[this.getCode()];
},
getIcons: function (type) {
return window.checkoutConfig.payment.ccform.icons.hasOwnProperty(type)
? window.checkoutConfig.payment.ccform.icons[type]
: false
},
getCcMonths: function() {
return window.checkoutConfig.payment.ccform.months[this.getCode()];
},
getCcYears: function() {
return window.checkoutConfig.payment.ccform.years[this.getCode()];
},
hasVerification: function() {
return window.checkoutConfig.payment.ccform.hasVerification[this.getCode()];
},
hasSsCardType: function() {
return window.checkoutConfig.payment.ccform.hasSsCardType[this.getCode()];
},
getCvvImageUrl: function() {
return window.checkoutConfig.payment.ccform.cvvImageUrl[this.getCode()];
},
getCvvImageHtml: function() {
return '<img src="' + this.getCvvImageUrl()
+ '" alt="' + $t('Card Verification Number Visual Reference')
+ '" title="' + $t('Card Verification Number Visual Reference')
+ '" />';
},
getSsStartYears: function() {
return window.checkoutConfig.payment.ccform.ssStartYears[this.getCode()];
},
getCcAvailableTypesValues: function() {
return _.map(this.getCcAvailableTypes(), function(value, key) {
return {
'value': key,
'type': value
}
});
},
getCcMonthsValues: function() {
return _.map(this.getCcMonths(), function(value, key) {
return {
'value': key,
'month': value.substring(0,2)
}
});
},
getCcYearsValues: function() {
return _.map(this.getCcYears(), function(value, key) {
return {
'value': key,
'year': value
}
});
},
getCurrentMonth: function() {
var d = new Date();
var n = d.getMonth() + 1;
return n;
},
getCurrentYear: function() {
var d = new Date();
var n = d.getYear();
return n;
},
getSsStartYearsValues: function() {
return _.map(this.getSsStartYears(), function(value, key) {
return {
'value': key,
'year': value
}
});
},
isShowLegend: function() {
return false;
},
getCcTypeTitleByCode: function(code) {
var title = '';
_.each(this.getCcAvailableTypesValues(), function (value) {
if (value['value'] == code) {
title = value['type'];
}
});
return title;
},
formatDisplayCcNumber: function(number) {
return 'xxxx-' + number.substr(-4);
},
getInfo: function() {
return [
{'name': 'Credit Card Type', value: this.getCcTypeTitleByCode(this.creditCardType())},
{'name': 'Credit Card Number', value: this.formatDisplayCcNumber(this.creditCardNumber())}
];
}
});
});
Here is the knockout HTML with select data-bind just in case it's needed (taken from Magento payment cc-form.html):
<select name="payment[cc_exp_month]"
class="select select-month"
data-bind="attr: {id: getCode() + '_expiration', 'data-container': getCode() + '-cc-month', 'data-validate': JSON.stringify({required:true, 'validate-cc-exp':'#' + getCode() + '_expiration_yr'})},
enable: isActive($parents),
options: getCcMonthsValues(),
optionsValue: 'value',
optionsText: 'month',
optionsCaption: $t('Month'),
value: creditCardExpMonth">
</select>
If this script is run every time the page loads, you could do something like this:
defaults: {
creditCardType: '',
creditCardExpYear: '',
creditCardExpMonth: (function(){
return ((new Date()).getMonth()+1);
})(),
creditCardNumber: '',
creditCardSsStartMonth: '',
creditCardSsStartYear: '',
creditCardVerificationNumber: '',
selectedCardType: null
}
or if you want something cleaner, you can refactor the code into a function that is defined prior to this object creation:
function (_, Component, creditCardData, cardNumberValidator, $t) {
function getCurrentMonth() {
return ((new Date()).getMonth()+1);
}
return Component.extend({
defaults: {
creditCardType: '',
creditCardExpYear: '',
creditCardExpMonth: getCurrentMonth(),
creditCardNumber: '',
creditCardSsStartMonth: '',
creditCardSsStartYear: '',
creditCardVerificationNumber: '',
selectedCardType: null
},
I am trying to move the code vm.canGoForward from my controller to a service to hide the implementation details.
BEFORE CODE CHANGE
This worked fine.
View:
<button ng-disabled="!vm.canGoForward()" class="btn btn-primary" name="next" type="button" ng-click="vm.gotoStep(vm.currentStep + 1)">
Controller:
var vm = this;
vm.currentStep = 1;
vm.steps = WizardService.getWizardSteps(vm.formData);
vm.canGoForward = function() {
var res = true,
i,
nextStateIndex = vm.currentStep + 1;
if (nextStateIndex > vm.steps.length) {
return false;
}
for (i = 1; res && i <= nextStateIndex; i++) {
res = (res && vm.steps[i-1].isReady());
}
return !!res;
};
Service
var wizardService = {
getWizardSteps: getWizardSteps
};
return wizardService;
function getWizardSteps(formData) {
var wizardSteps = [
{
step: 1,
name: 'Name',
template: 'views/wizard/step1.html',
isReady: function() { return true; }
},
{
step: 2,
name: 'Email',
template: 'views/wizard/step2.html',
isReady: function() { return formData.firstName && formData.lastName; }
},
{
step: 3,
name: 'Job Category',
template: 'views/wizard/step3.html',
isReady: function() { return formData.email; }
}
];
return wizardSteps;
}
AFTER CODE CHANGE
View
Remains the same
Controller
var vm = this;
vm.currentStep = 1;
vm.steps = WizardService.getWizardSteps(vm.formData);
vm.canGoForward = WizardService.canGoForward(vm.currentStep, vm.steps);
Service
var wizardService = {
getWizardSteps: getWizardSteps,
canGoForward: canGoForward
};
return wizardService;
function getWizardSteps(formData) {
var wizardSteps = [
{
step: 1,
name: 'Name',
template: 'views/wizard/step1.html',
isReady: function() { return true; }
},
{
step: 2,
name: 'Email',
template: 'views/wizard/step2.html',
isReady: function() { return formData.firstName && formData.lastName; }
},
{
step: 3,
name: 'Job Category',
template: 'views/wizard/step3.html',
isReady: function() { return formData.email; }
}
];
return wizardSteps;
}
function canGoForward(currentStep, steps) {
console.log(steps);
var res = true,
i,
nextStateIndex = currentStep + 1;
if (nextStateIndex > steps.length) {
return false;
}
for (i = 1; res && i <= nextStateIndex; i++) {
res = (res && steps[i-1].isReady());
}
return !!res;
}
I now get the following error: TypeError: v2.canGoForward is not a function. How can I resolve it?
In your second version, the following line will actually call WizardService.canGoForward on the spot, not assign it:
vm.canGoForward = WizardService.canGoForward(vm.currentStep, vm.steps);
What gets assigned is the return value of that call, which obviously is not a function, hence the error message when a call is attempted later.
If you want to assign the function, and ensure the arguments get passed when it is called later, then use bind:
vm.canGoForward = WizardService.canGoForward.bind(WizardService, vm.currentStep, vm.steps);
I see the basic example on github but I can't get it to work with my code. I should add that I'm using durandal.
How do I get the bindings to work? Am I doing anything wrong?
Input.js
define(['knockout'], function (ko) {
var ctor = function (value) {
//Properties
this.value = ko.observable(value);
this.placeholder = 'Input';
//Methods
this.getBindings = function () {
var bindings = {};
bindings.Input = {
value: this.value,
attr: {
placeholder: this.placholder,
},
};
bindings.Test = {
text: this.value,
};
return bindings;
};
};
return ctor;
});
Form.js
define(['knockout', 'Input'], function (ko, Input) {
var ctor = function (inputs) {
//Properties
this.inputs = ko.observableArray(inputs);
//Methods
this.getBindings = function () {
var bindings = {};
bindings.Inputs = {
foreach: this.inputs,
Item: function (context, classes) {
return context.$data.getBindings();
},
};
return bindings;
};
};
return ctor;
});
Module.js
define(['knockout', 'Input', 'Form'], function (ko, Input, Form) {
var ctor = function () { };
ctor.prototype.activate = function () {
var data = [
new Input(123),
new Input("Chris"),
new Input(true)
];
this.form = new Form(data);
};
ctor.prototype.binding = function () {
var bindings = this.form.getBindings();
ko.bindingProvider.instance.registerBindings(bindings);
};
return ctor;
});
Module.html This does not work.
<div id="Module">
<div data-class="Inputs">
<div>
<input data-class="Inputs.Item.Input" />
<span data-class="Inputs.Item.Test"></span>
</div>
</div>
</div>
Module.html This does work but I'm not using classBindingProvider for the foreach.
<div id="Module">
<div data-class="Inputs">
<div>
<input data-bind="value: value, attr: { placeholder: placeholder }" />
<span data-bind="text: value"></span>
</div>
</div>
</div>
There's no error message but the binding never happens. I just get 3 empty input fields.
I figured it out. I'll post the code that works.
I changed two things. First, I added <div data-class="Inputs.Item"> and then referenced the properties relative to that location (Input and Test). Second, I register the bindings immediately inside the getBindings functions, which will now turn them into initBindings.
Input.js
define(['knockout'], function (ko) {
var ctor = function (value) {
//Properties
this.value = ko.observable(value);
this.placeholder = 'Input';
//Methods
this.initBindings = function () { //FIX: getBindings => initBindings
var bindings = {};
bindings.Input = {
value: this.value,
attr: {
placeholder: this.placholder,
},
};
bindings.Test = {
text: this.value,
};
ko.bindingProvider.instance.registerBindings(bindings); //FIX: register instead of return
};
};
return ctor;
});
Form.js
define(['knockout', 'Input'], function (ko, Input) {
var ctor = function (inputs) {
//Properties
this.inputs = ko.observableArray(inputs);
//Methods
this.initBindings = function () { //FIX: getBindings => initBindings
var bindings = {};
bindings.Inputs = {
foreach: this.inputs,
Item: function (context, classes) {
context.$data.initBindings(); //FIX: Call the init.
},
};
ko.bindingProvider.instance.registerBindings(bindings); //FIX: register instead of return
};
};
return ctor;
});
Module.js
define(['knockout', 'Input', 'Form'], function (ko, Input, Form) {
var ctor = function () { };
ctor.prototype.activate = function () {
var data = [
new Input(123),
new Input("Chris"),
new Input(true)
];
this.form = new Form(data);
};
ctor.prototype.binding = function () {
this.form.initBindings(); //FIX: Call the init.
};
return ctor;
});
Module.html
<div id="Module">
<div data-class="Inputs">
<div data-class="Inputs.Item"> //FIX: no binding => Inputs.Item
<input data-class="Input" /> //FIX: Inputs.Item.Input => Input
<span data-class="Test"> //Fix: Inputs.Item.Test => Test
</span>
</div>
</div>
</div>
I'm trying to create a button class that extends an AbstractComponent class using $.extend() but the functions in AbstractComponent aren't available when I'm constructing the button.
The specific error I'm receiving is:
Uncaught TypeError: Object [object Object] has no method 'setOptions'
var Button = {};
var abstract = new AbstractComponent;
$.extend(Button,abstract);
//debugger;
//this.setOptions is available here
Button = function(options) {
'use strict';
var defaultOptions = {
templateName: '#button-tmpl',
title: "Label goes here",
type: "primary",
size: "medium",
disabled: null,
autosave: null,
href: null,
onclick: null
};
//debugger
//this.setOptions is not available here
this.setOptions(options, defaultOptions);
this.checkRequiredKeys('title');
return this;
};
Button.prototype.updateOptions = function() {
var options = this.options;
if (options.href === null) {
options.href = 'javascript:;';
}
if (options.disabled === null) {
options.disabled = 'disabled';
}
if (options.autosave === true) {
options.autosave = 'ping-autosave';
}
};
AbstractComponent.js
var AbstractComponent = function() {
console.log('this will be the constructor for elements extending this class');
};
AbstractComponent.prototype.show = function() {
this.render();
};
AbstractComponent.prototype.close = function() {
// stop listeners and remove this component
this.stopListening();
this.remove();
};
AbstractComponent.prototype.getTemplateName = function() {
return this.options.templateName;
};
AbstractComponent.prototype.checkRequiredKeys = function() {
var errors = new Array();
if (typeof this.getTemplateName() === "undefined") {
errors.push('templateName');
}
for (var i = 0; i < arguments.length; i++) {
if (!this.options.hasOwnProperty(arguments[i])) {
errors.push(arguments[i]);
}
}
if (errors.length > 0) {
throw new Exception("Required property(s) not found:" + errors.join(', ') + " in " + this.toString());
}
};
AbstractComponent.prototype.getElement = function() {
'use strict';
if(!this.options.updated) {
this.updateOptions();
}
return new AbstractView(this.options).render().$el;
};
AbstractComponent.prototype.updateOptions = function() {
this.options.updated = true;
return true;
};
AbstractComponent.prototype.getHtml = function() {
return this.getElement().html();
};
AbstractComponent.prototype.setOptions = function(options, defaultOptions) {
this.options = _.defaults(options, defaultOptions);
};
AbstractComponent.prototype.toString = function() {
return "Component" + this.getTemplateName() + "[id=" + this.options.id + "]";
};
jQuery extend is for moving properties from one (or more) object(s) to another object.
$.extend({}, {
foo: 10,
bar: 20
});
You should use prototypal inheritance isntead
function Button(options) {
'use strict';
var defaultOptions = {
templateName: '#button-tmpl',
title: "Label goes here",
type: "primary",
size: "medium",
disabled: null,
autosave: null,
href: null,
onclick: null
};
//debugger
//this.setOptions is not available here
this.setOptions(options, defaultOptions);
this.checkRequiredKeys('title');
return this;
};
Button.prototype = new AbstractComponent;