Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1864 simply productbundleoptions #2006

Merged
merged 7 commits into from
Nov 22, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions core/modules/catalog/components/ProductBundleOption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import rootStore from '@vue-storefront/store'

export const ProductBundleOption = {
name: 'ProductBundleOption',
props: {
option: {
type: Object,
required: true
},
errorMessages: {
type: Object,
default: null
}
},
data () {
return {
productOptionId: null,
quantity: 1
}
},
computed: {
productBundleOption() {
return `bundleOption_${this.option.option_id}`
},
bundleOptionName() {
return `bundleOption_${this._uid}_${this.option.option_id}_`
},
quantityName() {
return `bundleOptionQty_${this.option.option_id}`
},
value() {
return this.option.product_links.find(product => product.id === this.productOptionId)
},
errorMessage() {
return this.errorMessages ? this.errorMessages[this.quantityName] : ""
}
},
mounted() {
this.setDefaultValues()
if (rootStore.state.config.usePriceTiers) {
this.$bus.$on('product-after-setup-associated', this.setDefaultValues)
}
},
beforeDestroy () {
if (rootStore.state.config.usePriceTiers) {
this.$bus.$off('product-after-setup-associated', this.setDefaultValues)
}
},
watch: {
productOptionId(value) {
this.bundleOptionChanged()
},
quantity(value) {
this.bundleOptionChanged()
}
},
methods: {
setDefaultValues() {
if(this.option.product_links) {
const defaultOption = this.option.product_links.find(pl => { return pl.is_default })
this.productOptionId = defaultOption ? defaultOption.id : this.option.product_links[0].id
}
},
bundleOptionChanged() {
this.$emit('optionChanged', {
option: this.option,
fieldName: this.productBundleOption,
qty: this.quantity,
value: this.value
})
}
}
}
99 changes: 33 additions & 66 deletions core/modules/catalog/components/ProductBundleOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,6 @@ import * as types from '../store/product/mutation-types'
import rootStore from '@vue-storefront/store'
import i18n from '@vue-storefront/i18n'

function _defaultOptionValue (co, field = 'id') {
if (co.product_links && co.product_links.length) {
const defaultOption = co.product_links.find(pl => { return pl.is_default })
if (defaultOption) {
patzick marked this conversation as resolved.
Show resolved Hide resolved
return field === '*' ? defaultOption : defaultOption[field]
} else {
return field === '*' ? co.product_links[0] : co.product_links[0][field]
}
} else {
return field === '*' ? null : 0
}
}

function _fieldName (co) {
return ['bundleOption_' + co.option_id, 'bundleOptionQty_' + co.option_id]
}
Expand All @@ -30,25 +17,19 @@ export const ProductBundleOptions = {
},
data () {
return {
inputValues: {
},
selectedOptions: {
},
validation: {
rules: {},
results: {}
}
selectedOptions: {},
validationRules: {},
validationResults: {}
}
},
computed: {
/**
* Error messages map for validation options.
* TODO: Each option should be a separate component to avoid such complex logic.
*/
errorMessages () {
let messages = {}
Object.keys(this.validation.results).map(optionKey => {
const validationResult = this.validation.results[optionKey]
Object.keys(this.validationResults).map(optionKey => {
const validationResult = this.validationResults[optionKey]
if (validationResult.error) {
messages[optionKey] = validationResult.message
}
Expand All @@ -57,83 +38,69 @@ export const ProductBundleOptions = {
}
},
beforeMount () {
rootStore.dispatch('product/addCustomOptionValidator', {
validationRule: 'gtzero', // You may add your own custom fields validators elsewhere in the theme
validatorFunction: (value) => {
return { error: (value === null || value === '') || (value === false) || (value <= 0), message: i18n.t('Must be greater than 0') }
}
})

this.setupInputFields()

if (rootStore.state.config.usePriceTiers) {
this.$bus.$on('product-after-setup-associated', this.setupInputFields)
}
},
beforeDestroy () {
if (rootStore.state.config.usePriceTiers) {
this.$bus.$off('product-after-setup-associated', this.setupInputFields)
}
this.setupValidationRules()
},
methods: {
...mapMutations('product', {
setBundleOptionValue: types.CATALOG_UPD_BUNDLE_OPTION // map `this.add()` to `this.$store.commit('increment')`
}),
setupInputFields () {
setupValidationRules () {
rootStore.dispatch('product/addCustomOptionValidator', {
validationRule: 'gtzero', // You may add your own custom fields validators elsewhere in the theme
validatorFunction: (value) => {
return { error: (value === null || value === '') || (value === false) || (value <= 0), message: i18n.t('Must be greater than 0') }
}
})

for (let co of this.product.bundle_options) {
for (let fieldName of _fieldName(co)) {
this['inputValues'][fieldName] = _defaultOptionValue(co, fieldName.indexOf('Qty') > 0 ? 'qty' : 'id')
if (co.required) { // validation rules are very basic
this.validation.rules[fieldName] = 'gtzero' // TODO: add custom validators for the custom options
this.validationRules[fieldName] = 'gtzero' // TODO: add custom validators for the custom options
}
}
this.optionChanged(co, _defaultOptionValue(co, '*'))
}
},
optionChanged (option, opval = null) {
const fieldName = _fieldName(option)[0]
if (opval === null) {
const existingField = this.selectedOptions[fieldName]
if (existingField && existingField.hasOwnProperty('value') && typeof existingField.value === 'object') {
opval = existingField.value
}
}
const fieldNameQty = _fieldName(option)[1]
const value = opval === null ? this.inputValues[fieldName] : opval.id
this.setBundleOptionValue({ optionId: option.option_id, optionQty: parseInt(this.inputValues[fieldNameQty]), optionSelections: [value] })
optionChanged({fieldName, option, qty, value}) {
if (!fieldName) return
this.setBundleOptionValue({ optionId: option.option_id, optionQty: parseInt(qty), optionSelections: [value.id] })
this.$store.dispatch('product/setBundleOptions', { product: this.product, bundleOptions: this.$store.state.product.current_bundle_options }) // TODO: move it to "AddToCart"
this.selectedOptions[fieldName] = { value: (opval === null ? value : opval), qty: parseInt(this.inputValues[fieldNameQty]) }
if (this.validateField(option)) {
this.selectedOptions[fieldName] = {qty, value}
const valueId = value ? value.id : null
if (this.validateField(option, qty, valueId)) {
this.$bus.$emit('product-after-bundleoptions', { product: this.product, option: option, optionValues: this.selectedOptions })
}
},
isValid () {
let isValid = true
this.validation.results.map((res) => { if (res.error) isValid = false })
this.validationResults.map((res) => { if (res.error) isValid = false })
return isValid
},
validateField (option) {
validateField (option, qty, optionId) {
let result = true
let validationResult = { error: false, message: '' }
for (let fieldName of _fieldName(option)) {
const validationRule = this.validation.rules[fieldName]
const validationRule = this.validationRules[fieldName]
this.product.errors.custom_options = null
if (validationRule) {
const validator = this.$store.state.product.custom_options_validators[validationRule]
if (typeof validator === 'function') {
const validationResult = validator(this['inputValues'][fieldName])
this.validation.results[fieldName] = validationResult
const quantityValidationResult = validator(qty)
if(quantityValidationResult.error) validationResult = quantityValidationResult
const optionValidationResult = validator(optionId)
if(optionValidationResult.error) validationResult = optionValidationResult
this.$set(this.validationResults, fieldName, validationResult)
if (validationResult.error) {
this.product.errors['bundle_options_' + fieldName] = i18n.t('Please configure product bundle options and fix the validation errors')
this.product.errors['bundle_options_' + fieldName] = i18n.t('Please configure product custom options and fix the validation errors')
patzick marked this conversation as resolved.
Show resolved Hide resolved
result = false
} else {
this.product.errors['bundle_options_' + fieldName] = null
}
} else {
console.error('No validation rule found for ', validationRule)
this.validation.results[fieldName] = { error: false, message: '' }
this.$set(this.validationResults, fieldName, validationResult)
}
} else {
this.validation.results[fieldName] = { error: false, message: '' }
this.$set(this.validationResults, fieldName, validationResult)
}
}
return result
Expand Down
155 changes: 155 additions & 0 deletions src/themes/default/components/core/ProductBundleOption.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<template>
<div class="custom-option mb15">
<h4> {{ option.title }} </h4>
<div class="m5 relative" v-for="opval in option.product_links" :key="opval.id">
<input
type="radio"
class="m0 no-outline"
:name="bundleOptionName + opval.id"
:id="bundleOptionName + opval.id"
focus
:value="opval.id"
v-model="productOptionId"
>
<label v-if="opval.product" class="pl10 lh20 h4 pointer" :for="bundleOptionName + opval.id" v-html="opval.product.name" />
</div>
<div>
<label class="qty-label flex" :for="quantityName">{{ $t('Quantity') }}</label>
<input
type="number"
min="0"
class="m0 no-outline qty-input py10 brdr-cl-primary bg-cl-transparent h4"
:name="quantityName"
:id="quantityName"
focus
v-model="quantity"
>
</div>
<span class="error" v-if="errorMessage">{{ errorMessage }}</span>
</div>
</template>

<script>
import { ProductBundleOption } from '@vue-storefront/core/modules/catalog/components/ProductBundleOption.ts'

export default {
mixins: [ProductBundleOption]
}
</script>

<style lang="scss" scoped>
@import '~theme/css/variables/colors';
@import '~theme/css/helpers/functions/color';
$color-tertiary: color(tertiary);
$color-black: color(black);
$color-hover: color(tertiary, $colors-background);

$bg-secondary: color(secondary, $colors-background);
$color-secondary: color(secondary);
$color-error: color(error);
.qty-input {
border-style: solid;
border-width: 0 0 1px 0;
width: 90px;
}

.custom-option > label {
font-weight: bold;
margin-bottom: 10px;
}

.error {
color: $color-error;
padding-top: 5px;
display: block;
}
$color-silver: color(silver);
$color-active: color(secondary);
$color-white: color(white);

.relative label.qty {
padding-left: 5px;
}

.relative label {
padding-left: 35px;
margin-bottom: 12px;
cursor: pointer;
font-size: 16px;
line-height: 30px;
&:before {
content: '';
position: absolute;
top: 3px;
left: 0;
width: 22px;
height: 22px;
background-color: $color-white;
border: 1px solid $color-silver;
cursor: pointer;
}
}
input[type='text'] {
transition: 0.3s all;
&::-webkit-input-placeholder {
color: $color-tertiary;
}
&::-moz-placeholder {
color: $color-tertiary;
}
&:hover,
&:focus {
outline: none;
border-color: $color-black;
}
background: inherit;
}
input[type='radio'], input[type='checkbox'] {
position: absolute;
top: 3px;
left: 0;
opacity: 0;
&:checked + label {
&:before {
background-color: $color-silver;
border-color: $color-silver;
cursor: pointer;
}
&:after {
content: '';
position: absolute;
top: 9px;
left: 5px;
width: 11px;
height: 5px;
border: 3px solid $color-white;
border-top: none;
border-right: none;
background-color: $color-silver;
transform: rotate(-45deg);
}
}
&:hover,
&:focus {
+ label {
&:before {
border-color: $color-active;
}
}
}
&:disabled + label {
cursor: not-allowed;
&:hover,
&:focus {
&:before {
border-color: $color-silver;
cursor: not-allowed;
}
}
}
}
.qty-label {
font-size: 12px !important;
padding-left: 0px !important;
}
</style>
Loading