Advise on building web components in Shopify dawn theme - shopify

I am working on moving the functionality of an existing Shopify theme over to the dawn theme as a base, I want to incorporate the new code in the same pattern as the Dawn theme so it will mean transferring most of the existing code to web components.
I am new to web components so I am basically just looking for some advice or feedback from the first task so I don't make a series of similar mistakes.
The feature is an infinite scroll type load more button for collection pages. It uses the pagination next page liquid object (stored in a data attribute of the custom element) to load the next page products with a fetch request and also updates the data attribute so the next page is loaded.
This was a simple function before but this now seems quite complicated and I was wondering if I have taken the right approach here.
{% javascript %}
class InfiniteScroll extends HTMLUListElement {
constructor() {
super();
}
connectedCallback() {
this.productsOnPage = document.querySelector('[is=infinite-scroll]');
this.loadProductSetHandler = this.onLoadProductSet.bind(this);
this.loadMoreButton = document.querySelector('.infinite-scroll__load-more-button');
this.loadMoreSpinner = document.querySelector('.infinite-scroll__load-more .loading-overlay__spinner');
this.loadMoreButton.addEventListener('click', this.loadProductSetHandler, false);
}
disconnectedCallback() {
this.loadMoreButton.removeEventListener();
}
getNextProductSet() {
return this.productsOnPage.dataset.nextUrl;
}
setNextProductSet(nextProductSetUrl) {
this.productsOnPage.dataset.nextUrl = nextProductSetUrl;
}
loadProductSet(nextProductSet) {
this.productsOnPage.append(...nextProductSet);
this.loadMoreButton.classList.remove('hidden');
this.loadMoreSpinner.classList.add('hidden');
}
loadingProcess() {
this.loadMoreButton.classList.add('hidden');
this.loadMoreSpinner.classList.remove('hidden');
}
async fetchNextProductSet() {
let nextProductSetInfo = {};
await fetch(this.getNextProductSet())
.then(function(response) {
return response.text();
})
.then(function(html) {
let newProductSet = new DOMParser().parseFromString(html, "text/html").querySelector('[is=infinite-scroll]');
nextProductSetInfo = { newProducts: newProductSet.children, nextProductSet: newProductSet.dataset.nextUrl};
})
.catch(function(err) {
console.log('Failed to fetch page: ', err);
});
this.setNextProductSet(nextProductSetInfo.nextProductSet);
this.loadProductSet(nextProductSetInfo.newProducts);
}
onLoadProductSet(e) {
e.preventDefault();
this.loadingProcess();
this.fetchNextProductSet();
}
}
customElements.define('infinite-scroll', InfiniteScroll, { extends: "ul" });
{% endjavascript %}```

Related

How Do I Display Product Images In Shopware 6 Administration

I am working on a Shopware 6 Administrative plugin but displaying product images has been a big headache. I went through shopware repositories of 'product_media', 'media', and generally all folder repositories related to media.
I have not been able to decipher how image linking works since I can not get hold of the exact folder names.
How then do I go about this. Note: I have been able to get correct image names and relevant image data like id, folder id etc.
Below is my module/component idex.js file codes
import template from './images-page.html.twig';
const { Component, Context } = Shopware;
const { Criteria } = Shopware.Data;
Component.register('images', {
template,
inject: ['repositoryFactory', 'mediaService', 'acl'],
metaInfo() {
return {
title: 'images'
};
},
computed: {
/**productMediaRepository() {
return this.repositoryFactory.create(this.product.media.entity);
},*/
productRepository() {
return this.repositoryFactory.create('product');
},
mediaFolderRepository() {
return this.repositoryFactory.create('media_folder');
},
mediaRepository() {
return this.repositoryFactory.create('media');
},
rootFolder() {
const root = this.mediaFolderRepository.create(Context.api);
root.name = this.$tc('sw-media.index.rootFolderName');
root.id = null;
return root;
},
logRep(){
console.log(this.productRepository);
// console.log(this.mediaFolderRepository);
// console.log(this.mediaRepository);
// console.log(this.rootFolder);
}
},
methods: {
logProducts(){
const criteria = new Criteria();
this.productRepository
.search(criteria, Shopware.Context.api)
.then(result => {
console.log(result[0]);
});
},
logMediaFolders(){
const criteria = new Criteria();
this.mediaFolderRepository
.search(criteria, Shopware.Context.api)
.then(result => {
console.log(result);
});
}
},
created(){
this.logMediaFolders();
}
});
here's the twig template (nothing really here)
<sw-card title="Watermark">
<img src="" alt="Product Image" />
</sw-card>
The media elements of the products are associations that are not loaded automatically, but you can configure the criteria, so that those associations will be loaded directly when you fetch the products. Refer to the official docs for detailed infos.
In you case that means to load the cover image / or all product images, you would have to adjust the criteria you use for fetching the products the following way
logProducts(){
const criteria = new Criteria();
criteria.addAssociation('cover.media');
criteria.addAssociation('media.media');
this.productRepository
.search(criteria, Shopware.Context.api)
.then(result => {
console.log(result[0]);
});
},
Then to link to the cover image you can use:
<sw-card title="Watermark">
<img src="product.cover.media.url" alt="Product Image" />
</sw-card>
You find all the media elements of the product as an array under product.media and can also use product.media[0].media.url to link to those images.

How to add components programatically in vuejs (option api)

I am creating a map app using vue-leaflet and I'm trying to add paths when clicked on map.
I have everything set up but don't know how to add that l-polyline option in my template during runtime.
I tried to add it with DOM Api but it is taking it as it is without converting it to html
I want to add my component inside <l-map></l-map>
this is my map initialization
<l-map
class="mainMap"
ref="map"
v-if="isLocAvailable"
v-model="zoom"
v-model:zoom="zoom"
:center="currLocation"
#zoom="zoomUpperBound"
:options="{ zoomControl: false, maxZoom: 18 }"
#click="getCurrLoc"
>
and this is my getCurrLoc:
getCurrLoc(e) {
try {
const { lat, lng } = e.latlng;
if (this.onCreateMode === true) {
this.locations.push([lat, lng]);
}
} catch (err) {
console.log("what is err");
}
},
and i also have function for creating polyline in which i want to add that path adding functionality
createPolyline() {
console.log(this.locations);
//Code
//Reset Inputs
this.pathName = "";
this.onCreateMode = false;
this.locations = [];
},

Manipulate innerText of a CKEditor ViewElement

I am creating a little custom plugin for the CKEditor5 for the #neoscms.
Neos is using the #ckeditor5 but with a custom view.
The plugin is more or less a placeholder plugin. The user can configure a data-source with a key value store for items (identifier and labels). The dropdown in the CKEditor is filled with the items and when the user selects an item from the dropdown, it creates a placeholder element that should end in a span element with some data-attributes.
The main idea was to have an empty element and just data-attributes to identify the element and being able to assign live data. But it turns out that the live data thing is tricky. When I manipulate the span with an extra JS snippet on the Website, the CKEditor cannot handle this.
Is it possible to manipulate a view element in the DOM and still have a working Editor?
The Plugin works fine if I just add inner Text in the downCasting and don't replace something. But the live data would be nice.
Neos Backend with a element
Maybe that code gives an idea of the package.
It is not ready yet as this is more or less the main feature ;)
import {Plugin, toWidget, viewToModelPositionOutsideModelElement, Widget,} from "ckeditor5-exports";
import PlaceholderCommand from "./placeHolderCommand";
export default class PlaceholderEditing extends Plugin {
static get requires() {
return [Widget];
}
init() {
this._defineSchema();
this._defineConverters();
this.editor.commands.add(
"placeholder",
new PlaceholderCommand(this.editor)
);
this.editor.editing.mapper.on(
"viewToModelPosition",
viewToModelPositionOutsideModelElement(this.editor.model, (viewElement) =>
viewElement.hasClass("internezzo-placeholder")
)
);
this.editor.config.define("placeholderProps", {
types: ["name", "node", "nodePath"],
});
this.editor.config.define("placeholderBrackets", {
open: "[",
close: "]",
});
}
_defineSchema() {
const schema = this.editor.model.schema;
schema.register("placeholder", {
allowWhere: "$text",
isInline: true,
isObject: true,
allowAttributes: [
"name",
"node",
"nodePath",
"data-placeholder-identifier",
"data-node-identifier",
"data-node-path",
],
});
}
_defineConverters() {
const conversion = this.editor.conversion;
const config = this.editor.config;
conversion.for("upcast").elementToElement({
view: {
name: "span",
classes: ["foobar-placeholder"],
},
model: (viewElement, writer) => {
const name = viewElement.getAttribute('data-placeholder-identifier');
const node = viewElement.getAttribute('data-node-identifier');
const nodePath = viewElement.getAttribute('data-node-path');
const modelWriter = writer.writer || writer;
return modelWriter.createElement("placeholder", {name, node, nodePath, editable: false});
},
});
conversion.for("editingDowncast").elementToElement({
model: "placeholder",
view: (modelItem, writer) => {
const viewWriter = writer.writer || writer;
const widgetElement = createPlaceholderView(modelItem, viewWriter);
return toWidget(widgetElement, viewWriter);
},
});
conversion.for("dataDowncast").elementToElement({
model: "placeholder",
view: (modelItem, writer) => {
const viewWriter = writer.writer || writer;
return createPlaceholderView(modelItem, viewWriter);
},
});
// Helper method for downcast converters.
function createPlaceholderView(modelItem, viewWriter) {
const name = modelItem.getAttribute("name");
const node = modelItem.getAttribute("node");
const nodePath = modelItem.getAttribute("nodePath");
const placeholderView = viewWriter.createContainerElement("span", {
class: "foobar-placeholder",
"data-placeholder-identifier": name,
"data-node-identifier": node,
"data-node-path": nodePath,
});
// Would be nice to remove that and have just empty spans that get dynamic data
let innerText = config.get("placeholderBrackets.open") + name;
innerText += config.get("placeholderBrackets.close");
viewWriter.insert(
viewWriter.createPositionAt(placeholderView, 0),
viewWriter.createText(innerText)
);
return placeholderView;
}
}
}
So, the extra JS snippet that is used by the website is searching for spans with the class foobar-placeholder and writes a value with live data into the span. That works in the frontend, of course, but the backend of the CMS that uses CKEditor has issues with the changing data.
I could not find a solution with docs of CKEditor, and maybe I misuse the API somehow, but I now found a working solution for me.
My website snippet is now communicating with the Plugin via Broadcast messages. And then I search for placeholder elements and check if I need to change an attribute.
const broadcastChannel = new BroadcastChannel('placeholder:changeData');
broadcastChannel.postMessage({identifier: name, value});
And in the plugin
// Receive new values for placeholder via broadcast
const broadcastChannel = new BroadcastChannel('placeholder:changeData');
broadcastChannel.onmessage = (message) => {
const identifier = get('data.identifier', message);
const newValue = get('data.value', message);
this.editor.model.change( writer => {
if (identifier) {
this._replaceAttribute(writer, identifier, newValue);
}
});
};
Only downside now is that I need to reload the page, but already read that this is maybe cause by my element down casting and I change attributes.

How to convert Vue2 code to pure web components

I want to implement the autocomplete search (the one on the left) from this codepen to pure web components. But something went wrong because slots don't work and something else also doesn't work but I can't figure out what it is. What I have so far
Search-select
const template = `
<p>
<slot name="autocomp" results="${this.results}"
searchList="${(event) => this.setQuery(event)}"
>
fgfgfg
</slot>
yo
</p>
`;
class SearchSelect extends HTMLElement {
constructor() {
super();
this.query = "";
this.results = [];
this.options = [
"Inside Out",
"John Wick",
"Jurassic World",
"The Lord of the Rings",
"Pacific Rim",
"Pirates of the Caribbean",
"Planet of the Apes",
"Saw",
"Sicario",
"Zombies",
];
this.shadow = this.attachShadow({ mode: "open" });
}
setQuery(event) {
console.log(event.target);
this.query = event.target.value;
}
get options() {
return this.getAttribute("options");
}
set options(val) {
this.setAttribute("options", val);
}
static get observedAttributes() {
return ["options", "filterMethod"];
}
filterMethod(options, query) {
return options.filter((option) =>
option.toLowerCase().includes(query.toLowerCase())
);
}
attributeChangedCallback(prop, oldValue, newValue) {
if (prop === "options") {
this.results = this.filterMethod(this.options, this.query);
this.render();
}
if (prop === "filterMethod") {
this.results = this.filterMethod(this.options, this.query);
this.render();
}
}
render() {
this.shadow.innerHTML = template;
}
connectedCallback() {
this.render();
}
}
customElements.define("search-select", SearchSelect);
Autocomplete
const templ = `
<search-select>
<div class="autocomplete">
<input
type="text"
placeholder="Type to search list"
onchange="${this.searchList}"
onfocus="${this.showDropdown}"
onblur="${this.hideDropdown}"
/>
<div class="autocomplete-dropdown" v-if="dropdownVisible">
<ul class="autocomplete-search-results-list">
${this.result}
</ul>
</div>
</div>
</search-select>
`;
class Autocomplete extends HTMLElement {
constructor() {
super();
this.dropdownVisible = false;
this.rslts = "";
this.shadow = this.attachShadow({ mode: "open" });
}
get results() {
return this.getAttribute("results");
}
set results(val) {
this.setAttribute("results", val);
}
get searchList() {
return this.getAttribute("searchList");
}
showDropdown() {
this.dropdownVisible = true;
}
hideDropdown() {
this.dropdownVisible = false;
}
attributeChangedCallback(prop, oldValue, newValue) {
this.render();
}
render() {
this.shadow.innerHTML = templ;
}
connectedCallback() {
this.render();
}
}
customElements.define("auto-complete", Autocomplete);
Your current approach is completely wrong. Vue is reactive framework. Web components do not provide reactivity out of box.
The translation of Vue2 component to direct Web component is not straight forward. The slots do not work because Vue.js slots are not the same as Web component slots. They are just conceptually modeled after them.
First, when you use the Vue.js slot, you are practically putting some part of the vDOM (produced as a result of JSX) defined by the calling component into the Search or Autocomplete component. It is not a real DOM. Web components, on the other hand, provide slot which actually accepts a real DOM (light DOM).
Next, your render method is practically useless. You are simply doing this.shadow.innerHTML = template; which will simply append the string as HTML into the real DOM. You are not resolving the template nodes. Vue.js provides a reactivity out of box (that's why you need Vue/React). Web components do not provide such reactivity. On each render, you are re-creating entire DOM which is not a good way to do it. When you are not using any framework to build web component, you should construct all the required DOM in connectedCallback and then keep on selectively updating using DOM manipulation API. This is imperative approach to building UIs.
Third, you are using named slot while consuming it in auto complete, you are not specifying the named slot. So whatever is the HTML you see is not getting attached to the Shadow DOM.
You will need to
Building a complex component like Auto Complete needs a basic reactivity system in place that takes care of efficiently and automatically updating the DOM. If you do not need full framework, consider using Stencil, LitElement, etc. If you can use Vue.js, just use it and wrap it into Web component using helper function.
For Vue 2, you can use the wrapper helper library. For Vue 3, you can use the built-in helper.

Why are the views in my polymer app not getting loaded?

I am working on creating a Polymer app for a pet project, using the Polymer Starter Kit, and modifying it to add horizontal toolbar, background images, etc. So far, everything has worked fine except the links in the app-toolbar do not update the "view" when I click on them.
All my debugging so far points me in the direction of the "page" property. I believe this is not getting updated or is null, causing the view to default to "about" (which is View-2 as per the starter kit) as specified in the _routePageChanged observer method.
I tried using the debugger on DevTools on Chrome, but being new to this, I'm not very clear if I did it correctly. I just kept going in and out of hundred of function calls.
I am copying relevant parts of the app-shell.
Please help or at least point me in the right direction; I've been trying to fix this since 2 days. Thank you!
<app-location
route="{{route}}">
</app-location>
<app-route
route="{{route}}"
pattern=":view"
data="{{routeData}}"
tail="{{subroute}}">
</app-route>
<!-- Main content -->
<app-header-layout has-scrolling-region>
<app-header slot="header" class="main-header" condenses effects="waterfall">
<app-toolbar class="logo"></app-toolbar>
<app-toolbar class="tabs-bar" hidden$="{{!wideLayout}}">
<paper-tabs selected="[[selected]]" attr-for-selected="name">
<paper-tab>Home</paper-tab>
<paper-tab>About Us</paper-tab>
<paper-tab>Pricing</paper-tab>
</paper-tabs>
</app-toolbar>
</app-header>
<iron-pages
selected="[[page]]"
attr-for-selected="name"
fallback-selection="view404"
role="main">
<my-view1 name="home"></my-view1>
<my-view2 name="about"></my-view2>
<my-view3 name="pricing"></my-view3>
<my-view404 name="view404"></my-view404>
</iron-pages>
</app-header-layout>
</app-drawer-layout>
<script>
class MyApp extends Polymer.Element {
static get is() { return 'my-app'; }
static get properties() {
return {
page: {
type: String,
reflectToAttribute: true,
observer: '_pageChanged'
},
wideLayout: {
type: Boolean,
value: false,
observer: 'onLayoutChange'
},
items: {
type: Array,
value: function() {
return ['Home', 'About', 'Pricing', 'Adults', 'Contact'];
}
},
routeData: Object,
subroute: String,
// This shouldn't be neccessary, but the Analyzer isn't picking up
// Polymer.Element#rootPath
// rootPath: String,
};
}
static get observers() {
return [
'_routePageChanged(routeData.page)',
];
}
_routePageChanged(page) {
// If no page was found in the route data, page will be an empty string.
// Default to 'view1' in that case.
this.page = page || 'about';
console.log('_routePageChange');
// Close a non-persistent drawer when the page & route are changed.
if (!this.$.drawer.persistent) {
this.$.drawer.close();
}
}
_pageChanged(page) {
// Load page import on demand. Show 404 page if fails
var resolvedPageUrl = this.resolveUrl(page + '.html');
Polymer.importHref(
resolvedPageUrl,
null,
this._showPage404.bind(this),
true);
}
_showPage404() {
this.page = 'view404';
}
_onLayoutChange(wide) {
var drawer = this.$.drawer;
if (wide && drawer.opened){
drawer.opened = false;
}
}
}
window.customElements.define(MyApp.is, MyApp);
</script>
Here's a snapshot of the page when I click on the "Home" link.
Snapshot of the page
I have fixed the same issue on my app, page observed functions like:
static get properties() { return {
page:{
type:String,
reflectToAttribute:true,
observer: '_pageChanged'},
...
_pageChanged(page, oldPage) {
if (page != null) {
if (page === "home" ) {
this.set('routeData.page', "");
} else {
this.set('routeData.page', page);
}
.......
}
}
Honestly, I am still trying to find the better solution. Because I have users page and I could not manage to able to indexed at google search results. This only keeps synchronized the iron-pages and address link.