Shopware 6 Custom Component watch Plugin Config - vue.js

I am new to Shopware 6 and to vue.js as well. I would like to have a text-field in my plugin's config.xml, which will be disabled if another value in the config is true/false. Is there an event to subscribe? Can this be reactive (change disable state by only clicking the switch without having to save?). The switch should be able to disable a variable amount of input fields so I would not prefer to build a component just containing the switch and a corresponding text-input. Have you guys any hint?
For example:
I tried something like:
watch: {
'PluginName.config.disableswitch' (){
console.log("changed")
},
'$PluginName.config.disableswitch' (){
console.log("changed")
},
'PluginName.config.disableswitch': {
handler() {
console.log("changed");
},
},
'$PluginName.config.disableswitch': {
handler() {
console.log("changed");
},
},
},

My first suggestion is to install the VueJS extension on your browser, then inspect the Vue elements to know what data they get from the parents (it's not much).
So to pass down the data you want we will have to do just a tiny bit of work.
First, create a new plugin, e.g. src/custom/plugins/DockwareSamplePlugin
Then we need to rewrite the top level setting module to pass the config references you mentioned in your watcher.
Create a file src/Resources/app/administration/src/component/field-bind/settings/index.js
const {Component} = Shopware;
import template from './sw-system-config.html.twig'
Component.override('sw-system-config', {
template
});
You guessed it, in the same directory, create a file called src/Resources/app/administration/src/component/field-bind/settings/sw-system-config.html.twig
{% block sw_system_config_content_card_field %}
<sw-inherit-wrapper
v-model="actualConfigData[currentSalesChannelId][element.name]"
v-bind="getInheritWrapperBind(element)"
:has-parent="isNotDefaultSalesChannel"
:inherited-value="getInheritedValue(element)"
:class="'sw-system-config--field-' + kebabCase(getElementBind(element).name)"
>
<template #content="props">
<sw-form-field-renderer
v-bind="getElementBind(element, props)"
:key="props.isInheritField + props.isInherited"
:disabled="props.isInherited"
:value="props.currentValue"
#input="props.updateCurrentValue"
#change="props.updateCurrentValue"
:actualConfigData="actualConfigData[currentSalesChannelId]"
:myConfig="config[index]"
/>
</template>
</sw-inherit-wrapper>
{% endblock %}
Most of it is a copy from the original except the actualConfigData & myConfig attributes. This will insure that these props are passed to every field/component you create in your src/Resources/config/config.xml file.
Here is an example of such file implementation:
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/platform/master/src/Core/System/SystemConfig/Schema/config.xsd">
<card>
<title>#DockwareSamplePlugin# Settings</title>
<title lang="de-DE">#DockwareSamplePlugin# Einstellungen</title>
<input-field type="bool">
<name>customToggle</name>
<label>Config switch</label>
<specialType>disable</specialType><!-- this is an example of how we know what switch controls hiding -->
</input-field>
<component name="field-bind-text">
<name>customText</name>
<label>Text</label>
</component>
</card>
</config>
Next thing we need to create that component field-bind-text that gets shown/hidden.
Create a directory structure & file src/Resources/app/administration/src/component/field-bind/text/index.js
const {Component} = Shopware;
import template from './field-bind-text.html.twig'
Component.register('field-bind-text', {
template,
methods: {
getHideToggleName() {
return this.$attrs.myConfig.elements.find(element => element.config?.specialType === 'disable')['name']
}
},
computed: {
shouldDisable() {
return this.$attrs.actualConfigData[this.getHideToggleName()] ?? false
}
}
});
getHideToggleName - this gets the XML entry name I mentioned earlier, but you could also adjust to whatever you like it to be. Can create a custom component & look for that instead (e.g. element.config.componentName === 'my-component')
shouldDisable - this looks up the value of the XML toggle (by name)
Then the twig file src/Resources/app/administration/src/component/field-bind/text/field-bind-text.html.twig
<div class="some-class">
<sw-text-field :disabled="shouldDisable"></sw-text-field>
</div>
Oh, and dont forget to load it all with that src/Resources/app/administration/src/main.js file
import './component/field-bind/text'
import './component/field-bind/settings'
Alrighty! See, easy peazy lemon squeezie. Now you can quit & go back to working with mage2.

Related

Vue class components dynamically add component depending on answer from backend

So from the backend I get a array of objects that look kind of like this
ItemsToAdd
{
Page: MemberPage
Feature: Search
Text: "Something to explain said feature"
}
So i match these values to enums in the frontend and then on for example the memberpage i do this check
private get itemsForPageFeatures(): ItemsToAdd[] {
return this.items.filter(
(f) =>
f.page== Pages.MemberPage &&
f.feature != null
);
}
What we get from the backend will change a lot over time and is only the same for weeks at most. So I would like to avoid to have to add the components in the template as it will become dead code fast and will become a huge thing to have to just go around and delete dead code. So preferably i would like to add it using a function and then for example for the search feature i would have a ref on the parent like
<SearchBox :ref="Features.Search" />
and in code just add elements where the ItemsToAdd objects Feature property match the ref
is this possible in Vue? things like appendChild and so on doesn't work in Vue but that is the closest thing i can think of to kind of what I want. This function would basically just loop through the itemsForPageFeatures and add the features belonging to the page it is run on.
For another example how the template looks
<template>
<div class="container-fluid mt-3">
<div
class="d-flex flex-row justify-content-between flex-wrap align-items-center"
>
<div class="d-align-self-end">
<SearchBox :ref="Features.Search" />
</div>
</div>
<MessagesFilter
:ref="Features.MessagesFilter"
/>
<DataChart
:ref="Features.DataChart"
/>
So say we got an answer from backend where it contains an object that has a feature property DataChart and another one with Search so now i would want components to be added under the DataChart component and the SearchBox component but not the messagesFilter one as we didnt get that from the backend. But then next week we change in backend so we no longer want to display the Search feature component under searchbox. so we only get the object with DataChart so then it should only render the DataChart one. So the solution would have to work without having to make changes to the frontend everytime we change what we want to display as the backend will only be database configs that dont require releases.
Closest i can come up with is this function that does not work for Vue as appendChild doesnt work there but to help with kind of what i imagine. So the component to be generated is known and will always be the same type of component. It is where it is to be placed that is the dynamic part.
private showTextBoxes() {
this.itemsForPageFeatures.forEach((element) => {
let el = this.$createElement(NewMinorFeatureTextBox, {
props: {
item: element,
},
});
var ref = `${element.feature}`
this.$refs.ref.appendChild(el);
});
}
You can use dynamic components for it. use it like this:
<component v-for="item in itemsForPageFeatures" :is="getComponent(item.Feature)" :key="item.Feature"/>
also inside your script:
export default {
data() {
return {
items: [
{
Page: "MemberPage",
Feature: "Search",
Text: "Something to explain said feature"
}
]
};
},
computed: {
itemsForPageFeatures() {
return this.items.filter(
f =>
f.Page === "MemberPage" &&
f.Feature != null
);
}
},
methods: {
getComponent(feature) {
switch (feature) {
case "Search":
return "search-box";
default:
return "";
}
}
}
};

Vuejs & FilePond files is not working. How to transfer loaded file to an object?

I would like to use FilePond to Load images. But the current image in loading isn't transfered to my object. ( so i can use that object to send it to database ith axios)
My object is simply like that :
data:function() {
return {
image:'',
}}
Then, my FilePond component is like that:
<FilePond
name="test"
ref="pond"
:maxFiles="max || 1"
labelIdle="Drop files here..."
allowMultiple="false"
acceptedFileTypes="image/jpeg, image/png"
:files="image"
v-on:init="handleFilePondInit"/>
Thank you for you help if someone use FilePond with vuejs.
According to enter link description here data:image should be array object.
data: function() {
return {
image: ['']
};
},

Set a variable inside a v-for loop on Vue JS

I have a v-for loop with vue.js on a SPA and I wonder if it's posible to set a variable at the beginning and then just print it everytime you need it, because right now i'm calling a method everytime i need to print the variable.
This is the JSON data.
{
"likes": ["famiglia", "ridere", "caffè", "cioccolato", "tres leches", "ballare", "cinema"],
"dislikes":["tristezze", "abuso su animali", "ingiustizie", "bugie"]
}
Then I use it in a loop:
<template>
<div class="c-interests__item" v-for="(value, key) in interests" :key="key" :data-key="key" :data-is="getEmotion(key)" >
// NOTE: I need to use the variable like this in different places, and I find myself calling getEmotion(key) everythime, is this the way to go on Vue? or there is another way to set a var and just call it where we need it?
<div :class="['c-card__frontTopBox', 'c-card__frontTopBox--' + getEmotion(key)]" ...
<svgicon :icon="getEmotion(key) ...
</div>
</template>
<script>
import interests from '../assets/json/interests.json'
... More imports
let emotion = ''
export default {
name: 'CInfographicsInterests',
components: {
JSubtitle, svgicon
},
data () {
return {
interests,
emotion
}
},
methods: {
getEmotion (key) {
let emotion = (key === 0) ? 'happy' : 'sad'
return emotion
}
}
}
</script>
// Not relevanty to the question
<style lang='scss'>
.c-interests{...}
</style>
I tried adding a prop like :testy="getEmotion(key)" and then { testy } with no luck...
I tried printing { emotion } directly and it doesn't work
So, there is anyway to acomplish this or should i stick calling the method every time?
Thanks in advance for any help.
It's not a good idea to use methods inside a template for non-user-directed actions (like onClicks). It's especially bad, when it comes to performance, inside loops.
Instead of using a method, you can use a computed variable to store the state like so
computed: {
emotions() {
return this.interests.map((index, key) => key === 0 ? 'happy' : 'sad');
}
}
This will create an array that will return the data you need, so you can use
<div class="c-interests__item"
v-for="(value, key) in interests"
:key="key" />`
which will reduce the amount of times the item gets re-drawn.

Vue.js - Highmaps - Redraw map on series change

I have a highmaps 'chart' and the only thing that I want is to redraw the whole map inside an external function. Let me explain better. The map draws itself immediatly when the page loads up but I fetch some data from an external service and set it to a variable. Then I would like to just redraw the chart so that the new data appears in the map itself. Below is my code.
<template>
<div>
<highmaps :options="chartOptions"></highmaps>
</div>
</template>
<script>
import axios from 'axios';
import HighCharts from 'vue-highcharts';
import json from '../map.json'
let regions = [];
export default {
data: function () {
return {
chartOptions: {
chart: {
map: json, // The map data is taken from the .json file imported above
},
map: {
/* hc-a2 is the specific code used, you can find all codes in the map.json file */
joinBy: ['hc-key', 'code'],
allAreas: false,
tooltip: {
headerFormat: '',
pointFormat: '{point.name}: <b>{series.name}</b>'
},
series: [
{
borderColor: '#a0451c',
cursor: 'pointer',
name: 'ERROR',
color: "red",
data: regions.map(function (code) {
return {code: code};
}),
}
],
}
},
created: function(){
let app = this;
/* Ajax call to get all parameters from database */
axios.get('http://localhost:8080/devices')
.then(function (response) {
region.push(response.parameter)
/* I would like to redraw the chart right here */
}).catch(function (error){
console.error("Download Devices ERROR: " + error);
})
}
}
</script>
As you can see I import my map and the regions variable is set to an empty array. Doing this results in the map having only the borders and no region is colored in red. After that there is the created:function() function that is used to make the ajax call and retrieve data. After that I just save the data pushing it into the array and then obviously nothing happens but I would like to redraw the map so that the newly imported data will be shown. Down here is the image of what I would like to create.
If you have any idea on how to implement a thing like this or just want to suggest a better way of handling the problem, please comment.
Thanks in advance for the help. Cheers!
After a few days without any answer I found some marginal help online and came to a pretty satisfying conclusion on this problem so I hope it can help someone else.
So the first thing I did was to understand how created and mounted were different in Vue.js. I used the keyword created at first when working on this project. Because of that, inside this function, I placed my ajax call that gave me data which I then loaded inside the 'chart' by using the .addSeries method of the chart itself.
To reference the chart itself I used this: let chart: this.$refs.highcharts.chart. This searches for the field refs in any of your components/html elements and links it to the variable. So in the html there was something like this:
<template>
<div>
<highmaps :options="chartOptions" ref="highcharts"></highmaps>
</div>
</template>
The real problem was that the chart didn't even start rendering while all this process was going on so I changed the created keyword with mounted which means that it executes all the code when all of the components are correctly mounted and so my chart would be already rendered.
To give you (maybe) a better idea of what I am talking about I will post some code down below
mounted: function(){
let errorRegions = [];
let chart = this.$refs.highcharts.chart;
axios.get('localhost:8080/foo').then(function(response)
{
/* Code to work on data */
response.forEach(function(device){
errorRegions.push(device);
}
chart.addSeries({
name: "ERROR",
color: "red",
data: errorRegions
}
/* ...Some more code... */
})
}
And this is the result (have been adding some more series in the same exact manner)
Really hoping I have been of help to someone else. Cheers!

Twitter typeahead.js not working in Vue component

I'm trying to use Twitter's typeahead.js in a Vue component, but although I have it set up correctly as tested out outside any Vue component, when used within a component, no suggestions appear, and no errors are written to the console. It is simply as if it is not there. This is my typeahead setup code:
var codes = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('code'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
prefetch: contextPath + "/product/codes"
});
$('.typeahead').typeahead({
hint: true,
highlight: true,
minLength: 3
},
{
name: 'codes',
display: 'code',
source: codes,
templates: {
suggestion: (data)=> {
return '<div><strong>' + data.code + '</strong> - ' + data.name + '</div>';
}
}
});
I use it with this form input:
<form>
<input id="item" ref="ttinput" autocomplete="off" placeholder="Enter code" name="item" type="text" class="typeahead"/>
</form>
As mentioned, if I move this to a div outside Vue.js control, and put the Javascript in a document ready block, it works just fine, a properly formatted set of suggestions appears as soon as 3 characters are input in the field. If, however, I put the Javascript in the mounted() for the component (or alternatively in a watch, I've tried both), no typeahead functionality kicks in (i.e., nothing happens after typing in 3 characters), although the Bloodhound prefetch call is made. For the life of me I can't see what the difference is.
Any suggestions as to where to look would be appreciated.
LATER: I've managed to get it to appear by putting the typeahead initialization code in the updated event (instead of mounted or watch). It must have been some problem with the DOM not being in the right state. I have some formatting issues but at least I can move on now.
The correct place to initialize Twitter Typeahead/Bloodhound is in the mounted() hook since thats when the DOM is completely built. (Ref)
Find below the relevant snippet: (Source: https://digitalfortress.tech/js/using-twitter-typeahead-with-vuejs/)
mounted() {
// configure datasource for the suggestions (i.e. Bloodhound)
this.suggestions = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('title'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
identify: item => item.id,
remote: {
url: http://example.com/search + '/%QUERY',
wildcard: '%QUERY'
}
});
// get the input element and init typeahead on it
let inputEl = $('.globalSearchInput input');
inputEl.typeahead(
{
minLength: 1,
highlight: true,
},
{
name: 'suggestions',
source: this.suggestions,
limit: 5,
display: item => item.title,
templates: {
suggestion: data => `${data.title}`;
}
}
);
}
You can also find a working example: https://gospelmusic.io/
and a Reference Tutorial to integrate twitter typeahead with your VueJS app.