This is my first foray into using VueJS so any pointers or better ways to tackle the problem are much appreciated. Here is where I am at http://codepen.io/anon/pen/ezBwJw
I'm building a pricing plan table where users can browse 4 different payment plans. The table is interactive, users have access to radio buttons which toggle between viewing prices in GBP & USD, as well as viewing the cost if they pay per year or per month. All of this is working, but the issue I now have is that I want to pass some data to a 'summary' section which will be presented to the user before they choose to sign up. The one piece of data I am struggling to pass to the summary table is the price.
When a user selects a plan I want the price that is currently showing in that plan to show in the 'Total to pay now' field. In jQuery I'd do something like this (simplified version of Codepen)...
<div>
<h1>Basic</h1>
<div class="price">7</div>
Select this plan
</div>
<h2 class="total"></h2>
$('.select-plan').on('click', function() {
var planName = $(this).parent().find('.price').text();
$('.total').text(planName);
});
I'm currently using v-if to show the different prices for the respective plans, so I'm lost as to how I would get the item that is currently in view and pass that to the summary field.
JQuery Way
One option is to create watchers that call an updatePrice method whenever a variable that effects the current price changes. For example:
watch: {
'activePlan': 'updatePrice',
'currency': 'updatePrice',
'frequency': 'updatePrice'
},
... and then in methods:
updatePrice: function() {
this.price = $('.price.' + this.activePlan).text();
}
Here is a fork of your CodePen with that change. Notice that I've added the plan name as a class so that the JQuery selector can find the correct element.
Component Way (do this!)
Personally, I think it you'd be better off taking a totally different approach. I would make a custom component for plans. This will let you encapsulates all the functionality you require in a reusable and manageable way.
For example, you could make a component like this
Vue.component('plan-component', {
template: '#plan-component',
props: ['frequency', 'name', 'priceYearly', 'priceMonthly'],
computed: {
'price': function() {
// Move the logic for determining the price into the component
if (this.frequency === 'year') {
return this.priceYearly;
} else {
return this.priceMonthly;
}
}
},
methods: {
makeActivePlan() {
// We dispatch an event setting this to become the active plan
// A parent component or the Vue instance would need to listen
// to this event and act accordingly when it occurs
this.$dispatch('set-active-plan', this);
}
}
});
Components are related to an HTML template. So in your HTML you would need a template tag with id plan-component.
<template id="plan-component">
<h1>{{ name }}</h1>
<div>
<span>{{ price }}</span>
</div>
<a class="select-plan" v-on:click="makeActivePlan($event)" href="#">Select this plan</a>
</template>
Thus each plan gets its own component which handles the data related to that plan. And instead of repeating the same HTML for each plan in a table, you can just use your new custom <plan-component>, and bind the appropriate values to each plan (these are the props).
I've implemented this more fully as a JSFiddle here. I got rid of USB vs GBP currency because I wanted to keep things simple. I hope this gives you some idea about how to tackle your problem!
Related
I've got a VueJS application which filters items based on a number of checkbox items like a category filter for a shop.
When a user clicks a checkbox, we fire off an API request and a list of updated items is returned. The URL is also updated with a query string representing the checkbox that they have selected.
If a user navigates to a query stringed URL we want to have the checkboxes relating to the filters in the query string checked. That way if there is a page refresh, all the same checkboxes are checked.
We've done this so far using an if(window.location.search) and then parsing that query string, adding the parsed query string into an object. Passing that object down into the child component as a prop then setting the model the checkboxes are bound to to the query string object on update.
This works and is fine. The issue is theres stuttering and flashing of the checkboxes. You click the checkbox, it initially unchecks after selecting, the when the API response comes back, it select. Not very good for UX. I'm assuming this is because we're modifying the model the checkboxes are bound to while also trying to update it on checkbox click.
So I'm wondering if there's a better way of doing this please and if someone else has tackled a similar issue.
I've attached some code below, but as its spread across multiple components its quite hard to display here.
Thanks
<template>
<ul>
<li v-for="(filter, index) in filters" v-bind:key="index">
<input type="checkbox" name="filters" v-model="checked" v-on:change="changeItems">{{filter.filterName}}
</li>
{{checked}}
</ul>
</template>
<script>
export default {
data() {
return {
checked: []
}
},
props: [
'filters',
'checkedFilters' //passed object of filters in query string
],
updated: function() {
this.checked = this.checkedFilters
},
methods: {
changeItems: function (){
this.$emit('change-items', this.checked)
}
}
}
</script>
very new to react. you can say I have not yet started to think like React.
here is the problem:
<div>
<DropDown> </DropDown>
<Panel> </Panel>
</div>
In the dropdown, I select a value. Store it in state, as something as , currentLocation.
Then I go to Panel, hit a button, and I want to open a modal. When i open a modal, I need to pass the currentLocation to that model.
I can pass in arbitrary value to modal, but I cannot figure out a way to get the currently selected item from DropDown.
How do I get the value of the currently selected item to the Panel?
Am I even making sense?
When you call the setState in the dropdown that will force an update of the page.
Then if you call this.state in your component you should have the value you need there.
You should go over the basic tutorials to grasp the react basics.
But it goes like this:
getInitialState: function() {
return {
myVar: ''
//set your variables here
//getInitialState will get called before the component gets mounted
};
},
myCustomFunction: function(newVariable) {
this.setState({
myVar: newVariable
});
},
render: function() {
return (
<div>
<input
onChange={this.myCustomFunction}
/>
<MyCustomComponent myProp={this.state.myVar}/>
//everytime the state changes MyCustomComponent will have that new value as a prop
</div>
);
}
There is a lot of ambiguity in your question but I'll try for the simplest case.
You have a Panel component and a Dropdown component.
You want to the Panel to have access to a value that was set when the Dropdown was used.
Solution: When the Dropdown is actuated, it creates an Action that Stores the selected value.
When the modal button in the Panel is actuated, it creates an Action that requires the DropDownStore. Then it decides what to do based on that value.
The pattern I am loosely describing is known Facebook's Flux architecture which is basically just a more specific application architecture for React applications similar to pub/sub or an event bus.
I am trying to replace some text in an input field using JS but the view model overrides my commands each time. This is the HTML I start with:
<td class="new-variants-table__cell" define="{ editVariantPrice: new Shopify.EditVariantPrice(this) }" context="editVariantPrice" style="height: auto;">
<input type="hidden" name="product[variants][][price]" id="product_variants__price" value="25.00" bind="price" data-dirty-trigger="true">
<input class="mock-edit-on-hover tr js-no-dirty js-variant-price variant-table-input--numeric" bind-event-focus="onFocus(this)" bind-event-blur="onBlur(this)" bind-event-input="onInput(this)">
</td>
I run this JS:
jQuery('#product_variants__price').siblings().removeAttr('bind-event-focus');
jQuery('#product_variants__price').siblings().removeAttr('bind-event-input');
jQuery('#product_variants__price').siblings().removeAttr('bind-event-blur');
jQuery('#product_variants__price').siblings().focus()
jQuery('#product_variants__price').siblings().val("34.00");
jQuery('#product_variants__price').val("34.00");
And I'm left with the following HTML:
<td class="new-variants-table__cell" define="{ editVariantPrice: new Shopify.EditVariantPrice(this) }" context="editVariantPrice" style="height: auto;">
<input type="hidden" name="product[variants][][price]" id="product_variants__price" value="34.00" bind="price" data-dirty-trigger="true">
<input class="mock-edit-on-hover tr js-no-dirty js-variant-price variant-table-input--numeric">
</td>
The problem is that each time I click the input field the value is reverted to what it was when the page loaded.
I've also tried running the command in the parent td along with my value change, to simulate the editing of a variant and preventing default with no success:
jQuery('#product_variants__price').siblings().bind('input', function (e) {
e.preventDefault();
return false;
});
jQuery('#product_variants__price').siblings().bind('focus', function (e) {
e.preventDefault();
return false;
});
jQuery('#product_variants__price').siblings().focus()
jQuery('#product_variants__price').siblings().val("£34.00");
jQuery('#product_variants__price').val("£34.00");
jQuery('#product_variants__price').siblings().keydown()
Parent td function:
new Shopify.EditVariantPrice(jQuery('#product_variants__price').parent())
So how can I successfully edit this value in the inputs and also update the Shopify view model?
You can try this for yourself by going here:
https://jebus333.myshopify.com/admin/products/2521183043
login jebus333#mailinator.com
password shop1
EDIT: I've tried to find the view model on the page but with no success. Plus, there are no network calls when editing the values in the input fields, leading me to believe the values are being pulled back from somewhere on page.
Try this:
var old = Shopify.EditVariantPrice.prototype.onFocus;
Shopify.EditVariantPrice.prototype.onFocus = function(t) {
this.price = '50.00'; // Use the price you want here
old.call(this, t);
};
jQuery('#product_variants__price').siblings().triggerHandler("focus");
jQuery('#product_variants__price').siblings().triggerHandler("blur");
If it works for you, it's possible that the following will be sufficient:
Shopify.EditVariantPrice.prototype.onFocus = function(t) {
this.price = '50.00'; // Use the price you want here
};
Well, there is a kind of a dirty solution...
First of all you'll need a sendkeys plugin. In fact that means you'll need to include this and this JS libraries (you can just copy-paste them in the console to test). If you don't want to use the first library (I personally find it quite big for such a small thing) you can extract only the key things out of it and use only them.
The next step is creating the function which is going to act like a real user:
function input(field, desiredValue) {
// get the currency symbol while value is still pristine
var currency = field.val()[0];
// move focus to the input
field.click().focus();
// remove all symbols from the input. I took 10, but of course you can use value.length instead
for (var i = 0; i < 10; i++) field.sendkeys("{backspace}");
// send the currency key
field.sendkeys(currency);
// send the desired value symbol-by-symbol
for (var i = 0; i < desiredValue.length; i++) field.sendkeys(desiredValue[i]);
}
Then you can simply call it with the value you wish to assign:
input($("#product_variants__price").next(), "123.00");
I did not really manage to fake the blur event because of lack of the time; that is why I was forced to read the currency and pass .00 as a string. Anyway you already have a way to go and a quite working solution.
Looks like you're trying to automate editing of variant prices of products in Shopify's admin panel.
Instead of playing around with the DOM of Shopify's admin page, I'll suggest using Shopify's bulk product editor which lets you set prices of all variants in a single screen. I feel that you'll have better luck setting the variant prices using JavaScript on the bulk product editor page.
Clicking on the 'Edit Products' button as shown in the screenshot below will open the bulk product editor.
Also check if browser based macro recording plugins like iMacro can be of your help (you can also code macros with JS in iMacro).
So I have a modal box that allows the user to edit / save some data.
I just want to add that unlike other Meteor apps, I don't want to save the data straight away - I want the user to fill in all the fields before hitting save where it will save to the database and send to server etc. This is mainly because I want the user to be able to hit the "cancel" button to revert all changes.
I have a drop down box at the start of the form where depending on the value, fields will be shown or hidden
<select class="form-control" id="ddlNewInputType" placeholder="Enter your input type">
<option value="input">Input</option>
<option value="formula">Formula</option>
</select>
And I have a handlebar around a field like this to determine whether I want to show it
{{#if isFormula }}
<div class="row">
<input type="text"
id="txtNewInputFormula" placeholder="Enter formula">
</div>
{{/if}}
With a helper looking like this
isFormula: ->
$('#ddlNewInputType').val() == 'formula'
However, this doesn't work. Aside from when it first loads onto the page, it never hits isFormula, probably because Meteor doesn't consider any of the HTML elements as reactive so it never re-evaluates when the HTML element changes.
What is a suitable way to get around this? Is it possible to make something reactive explicitly in Meteor? I was also considering putting the dropdown list value into a session variable but that just seems messy because I'm going to need to manage this session variable (remember to clear it when the modal box closes etc.)
Your analysis is correct - a reactive variable needs to be involved in order for your helper to reevaluate after changing the select element. The basic strategy looks like:
Initialize a reactive variable when the template is created.
Whenever the select changes, update the reactive variable.
Read the reactive variable in your helper.
Rather than use a session variable, let's use a ReactiveVar scoped to your template. Here's an example set of modifications:
Template.myTemplate.helpers({
isFormula: function() {
return Template.instance().isFormula.get();
}
});
Template.myTemplate.events({
'change #ddlNewInputType': function (e, template) {
var isFormula = $(e.currentTarget).val() === 'formula';
template.isFormula.set(isFormula);
}
});
Template.myTemplate.created = function() {
// in your code, default this to the current value from
// your database rather than false
this.isFormula = new ReactiveVar(false);
};
Remember that you'll need to also do:
$ meteor add reactive-var
See my post on scoped reactivity for a full explanation of this technique.
As a practical exercise in learning bare-bones JS programming in depth (on up to date browsers), I am building an SPA to maintain customer records. The only external library I am using is Mithril.js MVC. So far I have got a table view with live data from my database, which includes edit, merge and delete buttons for each record. The editing is done and working well, using an inline "form" and save/cancel for that works.
I am now trying to implement delete and merge, both of which need a popup confirmation before being actioned, which is where I am stuck. I know exactly what I'd do in a desktop GUI environment, so the roadblock may be my lack of understanding of the browser front-end more than of Mithril, per se.
Ideally, I'd like to create a self-contained, reusable "popup" component represent the popup, but I can't see how I should go about doing this in JS using Mithril, in particular, but not solely, how to make Mithril to overlay one view on top of another.
Any assistance would be appreciated, from a broad outline to specific code snippets.
You probably want to use a view model flag to control the modal popup's visibility.
//modal module
var modal = {}
modal.visible = m.prop(false)
modal.view = function(body) {
return modal.visible() ? m(".modal", body()) : ""
}
//in your other view
var myOtherView = function() {
//this button sets the flag to true
m("button[type=button]", {onclick: modal.visible.bind(this, true)}, "Show modal"),
//include the modal anywhere it makes sense to
//its visibility is taken care by the modal itself
//positioning is controlled via CSS
modal.view(function() {
return m("p, "modal content goes here")
})
}
To make a modal dialog, you can either use the styles from one of the many CSS frameworks out there (e.g. Bootstrap), or style .modal with your own CSS
/*really contrived example to get you started*/
.modal {
background:#fff;
border:1px solid #eee;
position:fixed;
top:10px;
left:100px;
width:600px;
}
I don't know if I am just not quite getting MVC, but I simply set a view-model object that contains the detail of the popup, and then when generating the view if that is currently set I populate the div containing the popup. CSS controls the look and positioning.
So basically I am relying of Mithril's top-down re-render approach to conditionally build the view based on current application state -- it works really well and is immanently sensible to me.
I actually used a list of popup confirmation objects, so multiple confirmations can queue up.
In the controller, make a confirmation queue:
function Controller() {
...
this.confirmation =[];
...
}
In the view, create a confirmation view div if there's a confirmation queued, or an empty placeholder otherwise (Mithrils differencing works best if container elements don't appear and disappear from render to render):
function crtView(ctl) {
...
return m("div", [
...
crtConfirmationView(ctl),
...
]);
}
function crtConfirmationView(ctl) {
var cfm=ctl.confirmation[0];
return m("div#popup-confirm",(cfm ? muiConfirm.crtView(ctl,cfm.title,cfm.body,cfm.buttons) : null));
}
Then, whenever a confirmation is needed, just push a confirmation object into the queue and let Mithril's drawing system run and rebuild the view.
function deleteRecord(ctl,evt,row,idx,rcd) {
var cfm={
title : m("span","Delete Customer: "+rcd.ContactName),
body : [
m("p","Do you really want to delete customer "+rcd.CustomerId+" ("+rcd.ContactName+") and all associated appointments and addresses?"),
m("p.warning", "This action cannot be undone. If this is a duplicate customer, it should be merged with the other record."),
],
buttons : deleteButtons,
proceed : "delete",
index : idx,
record : rcd,
};
ctl.confirmation.push(cfm);
}
The confirmation object contains whatever properties that the confirm helper function crtView needs to create a confirmation view and then take action when the user clicks a button (or presses ENTER or ESCAPE, etc) -- just standard UI stuff that you abstract away into shared reusable components.
Note: Just in case anyone has questions about the array index, I have since moved away from using the array index to identify the record in the data model (when the delete is complete the array element should be removed). Instead I locate the affected record using database ID, which is resilient against intervening changes in the model, like sorting the list.