I import some text strings from a translation API. Some of these strings contain HTML - also links. Some of these links must link to internal router links. For example some link. Clicking this link will ofcourse work - but it reloads the app, instead of pushing the link within the SPA.
What's the best method to make imported/external links act like vue-router links?
You really should be rendering the links with <router-link> if you want the clicks to be handled by vue-router instead of by reloading the page.
Otherwise you can intercept the clicks (via delegation) and navigate to the new route manually:
<div #click="onClick">
<!-- Render the HTML in here -->
Link
</div>
onClick(e) {
if (e.target.tagName === 'A') {
e.preventDefault();
// Manually navigate to the route
this.$router.push(e.target.href);
}
}
Although it is an old question I ran into the same problem yesterday and might help for future reference.
Trying the way #Decade Moon answered did not solve my problem as the href tag of an anchor comes with the window.location.origin.
So if I was at https://www.example.com/about and want to navigate to the homepage I was getting this URL https://www.example.com/about/https://www.example.com which it was not the expected result.
So my solution was to pass a data attribute in my template string:
const toHomepage = `<span data-href="/">Link to Homepage</span>`;
And then in my component:
<p #click="linkTo" v-html="link"></p>
See that the v-html directive will output my template string.
linkTo(e) {
if (e.target.dataset.href) {
this.$router.push(e.target.dataset.href);
}
}
A more sophisticated version of Decade's answer based on https://dennisreimann.de/articles/delegating-html-links-to-vue-router.html:
function onClick(event: MouseEvent) {
let target = event.target as HTMLElement
while (target && target.tagName !== 'A') {
target = target.parentNode as HTMLElement
}
const href = (target as HTMLLinkElement).href
if (target && target.matches("a:not([href*='://'])") && href) {
const { altKey, ctrlKey, metaKey, shiftKey, button, defaultPrevented } = event
if (metaKey || altKey || ctrlKey || shiftKey) { return }
if (defaultPrevented) { return }
if (button !== undefined && button !== 0) { return }
if (target && target.getAttribute) {
const linkTarget = target.getAttribute('target') as string
if (/\b_blank\b/i.test(linkTarget)) return
}
const url = new URL(href)
const to = url.pathname
if (window.location.pathname !== to && event.preventDefault) {
event.preventDefault()
router.push(to)
}
}
Related
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.
I have a script I've used from codepen to create Tinder-like swipe cards. It all works fine except for one thing - I need to pass a querystring with variables so when the user swipes right they are taken to another page. In this section of code I can do this if the URL is plain like so:
if (!approved) {
position.x = -x;
position.rotation = -maxRotation;
icon.type = 'pass';
}else{
window.location.href = "recipe.html"
}
icon.opacity = 1;
setTimeout(() => this.showing = false, 200);
}
},
(full code is on the Codepen link above)
But if I try and pass vars to the querystring it doesn't work. I've tried:
window.location.href = "recipe.html?recipeid="+idMeal;
And
window.location.href = "recipe.html?recipeid="+{{ idMeal }};
And even
window.location.href = "recipe.html?recipeid="+`${idMeal}`;
But I can't get it to work. Any ideas would be gratefully received.
You should use,
router.push({ path: 'swipe', query: { plan: 'beauty lady' } })
documented here
https://router.vuejs.org/guide/essentials/navigation.html
I am using element ui el-image. And I want to preview my image when clicked with (:preview-src-list). But When I click first time it doesnt preview anything. just add's my downloaded image. So I need to click 2 times. But I want to click 1 time.
Here is my template code:
<el-image :src="src"
:preview-src-list="srcList"
#click="imgClick"></el-image>
ts code:
src = null;
srcList = [];
product = 'shoe1';
imgClick() {
prevImg(product).then(resp => {
const url = window.URL.createObjectURL(new Blob([resp.data]));
this.srclist = [url];
});
}
#Watch("product")
changed(value) {
getProductImage(value).then(resp => {
const url = window.URL.createObjectURL(new Blob([resp.data]));
this.src = url;
}).catc(e => {
alert(e);
});
}
mounted() {
this.changed(product);
}
I think these things happen because when you click on that image it will trigger clickHandler:
...
clickHandler() {
// don't show viewer when preview is false
if (!this.preview) {
return;
}
...
}
...
From source
And the preview is the computed property:
...
preview() {
const { previewSrcList } = this;
return Array.isArray(previewSrcList) && previewSrcList.length > 0;
}
...
From source
So nothing happened in the first click but after that you set preview-src-list and click it again then it works.
If you code is synchronous you can use event like mousedown which will trigger before click event.
<el-image
:src="url"
:preview-src-list="srcList"
#mousedown="loadImages">
</el-image>
Example
But if you code is asynchronous you can use refs and call clickHandler after that.
...
// fetch something
this.$nextTick(() => {
this.$refs.elImage.clickHandler()
})
...
Example
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.
I have this Vue Material (Vue.js) tag, with the function
<md-button id="" v-on:click.native="requestSelected(request)">
methods: {
requestSelected: function(request) {
request.accepted = true;
console.log(request);
var card = document.getElementById('text');
var accept = document.createTextNode("Job selected");
card.appendChild(accept);
}
I'm trying to add some text on the DOM after clicking, could someone recommend to me some Vue js documentacion to check info please
In your Vue component, create a data property for your display text:
data() {
return {
displayText: '',
}
}
Then, just put a reference to displayText in your template like so:
{{ displayText }}
Vue will initially display nothing, since displayText is empty, and the automatically update the DOM when displayText changes.
You would change the text in the requestSelected method like so:
requestSelected: function(request) {
request.accepted = true;
this.displayText = "Job selected";
}
Here's an example in codepen.