I'm working on Ember JS and I have a weird problem which I found no solution for.
Now I have a form, in this form I have a 'cost' input which takes obviously
a cost of an item, right next to it is a select tag from which the user
selects the time spending for the entered cost.
If a user spend 10 dollars on the specified item everyday then he should select "Daily" from the select menu, if the 10 dollars are spent "Weekly"
the user should select "weekly" and so on and so forth.
Now beneath these two inputs I have four "readonly" tags in which I want to show the user how much he spends "daily" "weekly" "monthly" and "yearly".
But I get an error "Assertion Failed You Modified **** twice ..."
Here is my template:
{{ui-input value=name placeholder="Expense Name" label="Name"}}
{{ui-input value=cost placeholder="Expense Cost" label="Cost" half=true}}
{{ui-select value=costTime default="Expense Time" label="Expense Time" items=formData.expenseTime half=true}}
<div class="col-md-6 no-padding">
{{ui-readonly value=expense.daily placeholder="Daily" label="Daily" half=true}}
{{ui-readonly value=expense.weekly placeholder="Weekly" label="Weekly" half=true}}
</div>
<div class="col-md-6 no-padding">
{{ui-readonly value=expense.monthly placeholder="Monthly" label="Monthly" half=true}}
{{ui-readonly value=expense.yearly placeholder="Yearly" label="Yearly" half=true}}
</div>
and here is what I'm trying to do in my controller
import Ember from 'ember';
export default Ember.Component.extend({
expense : { daily: '', weekly: '', monthly: '', yearly: '' },
setExpense : Ember.computed('cost', 'costTime', 'expense.weekly', function() {
if (this.get('costTime') == 'daily') {
this.set('expense.weekly', Number(this.get('cost') * 7))
}
return this.get('expense.weekly')
}),
});
I can see that you are trying to set a property expense.weekly inside a computed property setExpense which is considered as an anti-pattern because setting a property inside a computed one may trigger a UI rerender and it's already deprecated. Moreover, from the logic inside the computed property (setExpense), I assume that it is perfectly fit for an action.
You can move the logic to a separate action and trigger it when the select option changes. This will eliminate the backtracking errors which you are facing now.
Alternative:
You can compute expense with the help of computed property with the appropriate dependent properties.
expense: Ember.computed('cost', 'costTime', function () {
let expense = { daily: '', weekly: '', monthly: '', yearly: '' };
// compute the expense here...
return expense;
})
Best pattern is to use DDAU and component didReceiveAttrs hook.
import Ember from 'ember';
export default Ember.Component.extend({
cost: null,
costTime: null
expense : { daily: '', weekly: '', monthly: '', yearly: '' },
didReceiveAttrs: function() {
var cost = this.get('cost');
var costTime = this.get('costTime');
if (cost && costTime) {
if (costTime == 'daily') {
this.set('expense.weekly', Number(cost * 7))
} else ...
}
}
});
and you must pass actions on change cost and costTime to parent component and change it there.
edit: Another approach is to define more properties:
import Ember from 'ember';
var multipliers = { daily: 1, weekly: 7, monthly: 30, yearly: 365 };
function computeDailyCost(cost, costTime) {
var multiplier = multipliers[costTime];
if (multiplier) {
return cost / multiplier
}
}
function defMultCost(multiplierKey) {
return Ember.computed("cost", "costTime", function() {
var dailyCost = this.computeDailyCost(this.get("cost"), this.get("costTime"));
var multiplier = multipliers[multiplierKey];
if (dailyCost != null && multiplier != null) {
return dailyCost * multiplier;
}
});
};
export default Ember.Component.extend({
cost: null,
costTime: null
daily: defMultCost("daily"),
weekly: defMultCost("weekly"),
monthly: defMultCost("monthly"),
yearly: defMultCost("yearly")
});
I did not test this, but it should work.
Related
I'm brand new at VueJs and JS in general, so perhaps my answer is obvious, but I have a page where I would like a text input box to only appear when user selects the document type 'Write Individual'. However, I'm not sure I'm writing this syntactically accurate as it's not providing me with expected behavior. I'd appreciate tips as to what is the best way forward!
Vue
<template v-if="attachmentAccepted">
<b-field
:label="t('document_type')"
>
<b-select
v-model="documentType"
icon="file-alt"
:disabled="waiting.updateAttachment"
>
<optgroup
v-for="group in allDocumentTypeGroups"
:key="group.id"
:label="t(group.name)"
>
<option
v-for="type in group.documentTypes"
:key="type.id"
:value="type.id"
>
{{(type.name)}}
</option>
</optgroup>
</b-select>
</b-field>
and this is where I've really been struggling, the document types appear to be received in a JSON, so my next step would be the view the parsed data so I can accurately reference it in my v-if statement, but being new at Vue I have zero clue as to how to view the parsed data. console.log(this.documentTypes) hasn't worked.
.then(({data}) => {
this.participants = data.participants;
this.installmentData = data.installments;
this.installmentData = data.installments;
this.installmentData.paymentDateStart = moment(data.installments.paymentDateStart).format('YYYY-MM-DD');
this.installmentData.paymentDateEnd = moment(data.installments.paymentDateEnd).format('YYYY-MM-DD');
this.documentTypes = JSON.parse(data.documentTypes);
this.formattedDocumentTypes = JSON.parse(data.formattedDocumentTypes);
this.loading = false;
})
edit: props + data
export default {
name: 'AttachmentEditForm',
mixins: [AttachmentTypeMixin],
components: { DocumentCheckbox },
props: {
attachment: Object,
slideIndex: Number,
},
data() {
return {
waiting: {},
}
},
You need to first define your data in data function :
data() {
return {
waiting: {},
participants: null,
installmentData: {
paymentDateStart: null,
paymentDateEnd: null,
},
documentTypes: null,
formattedDocumentTypes: null,
loading: false
}
},
First, to console your documentTypes, why not to do console.log(data.documentTypes) in .then()?
Second. regarding the conditional showing the text field. You are absolutely right about using v-if, you just need to decide which documentType value is supposed to make the field visible and check it. In turn, this documentType is set by selecting an item in the list, and it should be in your data{} section, btw (I couldn't find it there). So, something like this:
<template v-if="attachmentAccepted">
<b-field
v-if="documentType == 'some_value_to_make_this_true'"
:label="t('document_type')"
>
<b-select
v-model="documentType"
icon="file-alt"
:disabled="waiting.updateAttachment"
>
<optgroup
v-for="group in allDocumentTypeGroups"
...
export default {
name: 'AttachmentEditForm',
mixins: [AttachmentTypeMixin],
components: { DocumentCheckbox },
props: {
attachment: Object,
slideIndex: Number,
},
data() {
return {
documentType: null,
waiting: {},
}
},
I have a child component which consists of 3 checkboxes (generated dynamically using ngFor) and an Apply and Cancel button.
Selector tag for the child is added in the parent's template. Parent component accesses this child component using #ViewChild and calls the present() method exposed by the child component with an object as argument as below which consists of checked state of the checkboxes.
Every time when modal is displayed, present() method is getting called. For the first time, UI/checkboxes is getting updated/checked as the values sent by parent. But, in the subsequent calls to present(), even though options.checked value is getting updated as expected in the ts file, this is not getting reflected in the UI. Every time the modal is displayed, I want checkbox to be checked or unchecked based on the value sent by the parent in present() method. Need help. Thanks in advance
parent.component.ts:
#ViewChild(childModalComponent) childModalComponent: ChildModalComponent;
onBtnClick() {
this.childModalComponent.present({
checkbox1: true,
checkbox2: false,
checkbox3: false
});
}
parent.component.html:
<feature-child-modal></feature-child-modal>
child.component.ts:
#ViewChild('childModal') childModal: ElementRef;
ngOnInit() {
this.options = [
{
label: 'label1',
value: 'value1',
checked: false,
},
{
label: 'label2',
value: 'value2',
checked: false,
},
{
label: 'label3',
value: 'value3',
checked: false,
},
];
}
present(optionsState: CompressTransactionType) {
this.options.forEach(item => {
if(item.value == "value1"){
item.checked = optionsState.checkbox1;
}
if(item.value == "value2"){
item.checked = optionsState.checkbox2;
}
if(item.value == "value3"){
item.checked = optionsState.checkbox3;
}
});
this.childModal.nativeElement.present();
}
dismiss() {
this.childModal.nativeElement.dismiss();
}
child.component.html:
<div *ngFor="let option of options">
<input
type="checkbox"
[value]="option.value"
(change)="onOptionsSelectChanged($event)"
[checked]="option.checked" />
</div>
try passing the array object here option instead of $event. Have a look in this url..
https://stackblitz.com/edit/angular-ivy-wifdeg
SAMPLE https://stackblitz.com/edit/usjgwp?file=index.html
I want to show a number of kendo dropdownlist(s) on a page. The exact number depends on an API call. This API call will give me an array of stakeholder objects. Stakeholder objects have the following properties: Id, name, type, role and isSelected.
The number of dropdownlist that has to be shown on this page should be equal to the number of unique type values in the API response array. i.e,
numberOfDropdowns = stakeholders.map(a => a.type).distinct().count().
Now, each dropdown will have a datasource based on the type property. i.e, For a dropdown for type = 1, dataSource will be stakeholders.filter(s => s.type == 1).
Also the default values in the dropdowns will be based on the isSelected property. For every type, only one object will have isSelected = true.
I have achieved these things by using the following code:
<template>
<div
v-if="selectedStakeholders.length > 0"
v-for="(stakeholderLabel, index) in stakeholderLabels"
:key="stakeholderLabel.Key"
>
<label>{{ stakeholderLabel.Value }}:</label>
<kendo-dropdownlist
v-model="selectedStakeholders[index].Id"
:data-source="stakeholders.filter(s => s.type == stakeholderLabel.Key)"
data-text-field="name"
data-value-field="Id"
></kendo-dropdownlist>
<button #click="updateStakeholders">Update form</button>
</div>
</template>
<script>
import STAKEHOLDER_SERVICE from "somePath";
export default {
name: "someName",
props: {
value1: String,
value2: String,
},
data() {
return {
payload: {
value1: this.value1,
value2: this.value2
},
stakeholders: [],
selectedStakeholders: [],
stakeholderLabels: [] // [{Key: 1, Value: "Stakeholder1"}, {Key: 2, Value: "Stakeholder2"}, ... ]
};
},
mounted: async function() {
await this.setStakeholderLabels();
await this.setStakeholderDataSource();
this.setSelectedStakeholdersArray();
},
methods: {
async setStakeholderLabels() {
let kvPairs = await STAKEHOLDER_SERVICE.getStakeholderLabels();
kvPairs = kvPairs.sort((kv1, kv2) => (kv1.Key > kv2.Key ? 1 : -1));
kvPairs.forEach(kvPair => this.stakeholderLabels.push(kvPair));
},
async setStakeholderDataSource() {
this.stakeholders = await STAKEHOLDER_SERVICE.getStakeholders(
this.payload
);
}
setSelectedStakeholdersArray() {
const selectedStakeholders = this.stakeholders
.filter(s => s.isSelected === true)
.sort((s1, s2) => (s1.type > s2.type ? 1 : -1));
selectedStakeholders.forEach(selectedStakeholder =>
this.selectedStakeholders.push(selectedStakeholder)
);
},
async updateStakeholders() {
console.log(this.selectedStakeholders);
}
}
};
</script>
The problem is that I am not able to change the selection in the dropdownlist the selection always remains the same as the default selected values. Even when I choose a different option in any dropdownlist, the selection does not actually change.
I've also tried binding like this:
<kendo-dropdownlist
v-model="selectedStakeholders[index]"
value-primitive="false"
:data-source="stakeholders.filter(s => s.type == stakeholderLabel.Key)"
data-text-field="name"
data-value-field="Id"
></kendo-dropdownlist>
If I bind like this, I am able to change selection but then the default selection does not happen, the first option is always the selection option i.e, default selection is not based on the isSelected property.
My requirement is that I have to show the dropdown with some default selections, allow the user to choose different options in all the different dropdowns and then retrieve all the selection then the update button is clicked.
UPDATE:
When I use the first method for binding, The Id property of objects in the selectedStakeholders array is actually changing, but it does not reflect on the UI, i.e, on the UI, the selected option is always the default option even when user changes selection.
Also when I subscribe to the change and select events, I see that only select event is being triggered, change event never triggers.
So it turns out that it was a Vue.js limitation (or a JS limitation which vue inherited),
Link
I had to explicitly change the values in selectedStakeholders array like this:
<template>
<div
v-if="selectedStakeholders.length > 0"
v-for="(stakeholderLabel, index) in stakeholderLabels"
:key="stakeholderLabel.Key"
>
<label>{{ stakeholderLabel.Value }}:</label>
<kendo-dropdownlist
v-model="selectedStakeholders[index].Id"
:data-source="stakeholders.filter(s => s.type == stakeholderLabel.Key)"
data-text-field="name"
data-value-field="Id"
#select="selected"
></kendo-dropdownlist>
<button #click="updateStakeholders">Update form</button>
</div>
</template>
And in methods:
selected(e) {
const stakeholderTypeId = e.dataItem.type;
const selectedStakeholderIndexForTypeId = this.selectedStakeholders.findIndex(
s => s.type == stakeholderTypeId
);
this.$set(
this.selectedStakeholders,
selectedStakeholderIndexForTypeId,
e.dataItem
);
}
I am working on a survey with Vue. I am using an array for all the questions and an index to navigate through them and display them one at a time. I'm using different input types for each question, e.g. number, radio, text etc. For some questions I'll need more than one input. I am using v-bind to pass the type of the question.
Now the problem that I encounter is that I'll need more than one input per question, e.g. when passing radio button I only get one when I need 2+. Same for labels for the buttons. I have also realized that I'm going to need two different input types for some questions (e.g. both input number and radio).
This is my working fiddle, to give you an idea of what I'm trying to accomplish. I would like to know if this is doable with my current approach, or if I need to use components for the questions and use different templates for each of them and how I would go about doing that.
I am writing this for the second time from memory since the first time I got an error, so I apologize if I failed to mention something important. Thanks in advance!
new Vue({
el: '#quizz',
data: {
questions:[
{question: 'What is your gender?', answer: '', type: 'radio', checked: 'true', label: 'Male'},
{question:'How old are you?', answer: '', type: 'number', checked: 'false'},
{question:'How many times do you workout per week?', answer: '', type: 'number', checked: 'false'},
],
index:0
},
computed:{
currentQuestion(){
return this.questions[this.index]
}
},
methods:{
next(){
if(this.index + 1 == this.questions.length)
this.index = 0;
else
this.index++;
},
previous(){
if(this.index - 1 < 0)
this.index = this.questions.length - 1;
else
this.index--;
}
}
})
I would probably handle this by building question "type" components. For example,
const RadioQuestion = {
props:["value", "question"],
template:`
<div>
<template v-for="label in question.labels">
<input type="radio" :id="label" :value="label" v-model="picked">
<label :for="label">{{label}}</label>
<br>
</template>
</div>
`,
data(){
return {
picked: this.value
}
},
watch:{
picked(){
this.$emit("input", this.picked)
}
}
}
const NumericInputQuestion = {
props:["value", "question"],
template:`
<div>
<input type="number" v-model="internalValue" #input="onInput" :value="value" />
</div>
`,
data(){
return {
internalValue: this.value
}
},
methods:{
onInput(){this.$emit("input", this.internalValue)}
}
}
Then build your data like this.
data: {
questions:[
{question: 'What is your gender?', type: RadioQuestion, labels:["Male", "Female"], answer: null},
{question:'How old are you?', type: NumericInputQuestion, answer: null},
{question:'How many times do you workout per week?', type: NumericInputQuestion, answer: null}
],
index:0
}
Modify your template accordingly.
<div id="quizz" class="question">
<h2>
{{ currentQuestion.question }}
</h2>
<component :key="currentQuestion" :is="currentQuestion.type" :question="currentQuestion" v-model="currentQuestion.answer"></component>
Current Question Answer: {{ currentQuestion.answer }}
<div class='button' id='next'>Next</div>
<div class='button' id='prev'>Prev</div>
</div>
Here is an updated fiddle demonstrating the technique.
I've really hit a brick wall with this, and I know I'm probably missing something here, but I'm stuck and need help. What I'm trying to do is use a service to populate the options in an ng-options directive; however, the ng-options are inside of a custom directive, and I've tried everything from track by, to testing it outside of the directive, inside the directive, etc. Can someone please take a look at this code and see if you can spot what I'm missing? Any help is greatly appreciated. It WILL work as far as executing the update to the ng-model; however, at page landing and record selection, it will not initially select the proper option, but if I take the track by out, it will initialize with the proper selection, it just won't update ng-model when/if I do that.
angular
.module('app')
.controller('mainCtrl', ['acctList', 'CONSTANTS', 'FORMFIELDS', function(acctList, CONSTANTS, FORMFIELDS) {
var mainCtrl = this;
mainCtrl.form = {};
mainCtrl.formFields = FORMFIELDS;
mainCtrl.currentRecord = null;
mainCtrl.editedRecord = {};
mainCtrl.setCurrentRecord = function(value) {
mainCtrl.currentRecord = value;
mainCtrl.editedRecord = angular.copy(mainCtrl.currentRecord);
};
mainCtrl.statuses = CONSTANTS.statuses;
}])
.value('FORMFIELDS', [
{
key: 'active_flag',
inputtype: 'select',
type: 'text',
class: 'form-control',
id: 'activeFl',
name: 'activeFl',
placeholder: 'Active Flag',
required: true,
maxlength: 1,
disabled: false,
labelfor: 'inputActiveFl',
labeltext: 'Active Flag',
field: 'mainCtrl.editedRecord.ACTIVE_FL',
options: 'list as list.desc for list in mainCtrl.statuses track by list.value'
}
])
.value('CONSTANTS',
{
statuses: [
{
id: 1,
value: "Y",
desc: "Active"
},
{
id: 2,
value: "N",
desc: "Inactive"
}
]
}
)
.directive('formTemplate', ['$compile', function($compile) {
function linker(scope, element, attr) {
scope.$watch(attr.modeltemp, function(modeltemp) {
// if ngModel already equals modeltemp or modeltemp doesn't exist, return
if (attr.ngModel == modeltemp || !modeltemp) return;
// remove all attributes to prevent duplication
element.removeAttr('placeholder');
element.removeAttr('type');
element.removeAttr('class');
element.removeAttr('id');
element.removeAttr('name');
element.removeAttr('ng-required');
element.removeAttr('maxlength');
element.removeAttr('ng-disabled');
// add the ng-model attribute presently tied to modeltemp
element.attr('ng-model', modeltemp);
// if modeltemp is blank, then remove ng-model, as it would be null
if (modeltemp == '') {
element.removeAttr('ng-model');
}
// Unbind all previous event handlers, this is
// necessary to remove previously linked models.
element.off();
// run a compile on the element, injecting scope, to reconstruct the element
$compile(element)(scope);
});
console.log(scope.acctCtrl);
}
// dynamic templating function associated with the templateUrl in the DDO
function template (tElement, tAttrs) {
// set the type variable equal to the value from the tAttr for 'inputtype' coming from the view
var type = tAttrs['inputtype'];
// just declaring the return variable for cleanliness
var tpl;
// begin the switch-case statement for each inputtype, then set it's return variable equal to the respective url
switch(type) {
case 'input':
tpl = '/common/directives/formTemplate/formTemplate.template.html';
break;
case 'select':
tpl = '/common/directives/formTemplate/formTemplateSelect.template.html';
break;
default:
tpl = '/common/directives/formTemplate/formTemplate.template.html';
break;
}
return tpl;
}
return {
restrict: 'EA',
replace: true,
templateUrl: template,
link: linker
};
}])
<form class="form-horizontal" ng-submit="submit()" name="mainCtrl.form.newAcctForm">
<div class="col-lg-6 form-fields" ng-repeat="fields in mainCtrl.formFields" ng-class="{ 'has-error': mainCtrl.form.newAcctForm.{{fields.name}}.$dirty }">
<label class="control-label" for="{{fields.labelfor}}">{{fields.labeltext}}</label>
<div form-template modeltemp="fields.field" inputtype="{{fields.inputtype}}"></div>
</div>
</form>
<select class="{{fields.class}}" id="{{fields.id}}" name="{{fields.name}}" ng-options="{{fields.options}}" ng-required="{{fields.required}}" maxlength="{{fields.maxlength}}" ng-disabled="{{fields.disabled}}">
<option value="">Please select...</option>
</select>
While this does work, did you consider using lifecycle hooks instead, waiting until after the view has loaded/initialized? Your solution works, but it's a bit like using a rocket launcher on an ant hill.