How to get slots's html content as string in vue3? - vuejs3

I want to pass slot's content into a wrap components which one of it's prop will accept html string and render it, here's the code
Parent.vue
<Ellipsis style="max-width: 240px"
>Genes that helped people survive during the time of the Black Death are
more likely to be found in people with autoimmune diseases alive today.
Does this tell scientists anything about what surviving the COVID-19
pandemic might mean for the world’s population?
<template #tooltip>
<div style="text-align: center">
<i>Lorem Ipsum</i><br />
Sed ut perspiciatis unde omnis<br />
iste natus error sit voluptatem accusantium doloremque
laudantium,<br />
totam rem aperiam
</div>
</template>
</Ellipsis>
Ellipsis.vue
<template>
<Tooltip
ref="tooltipRef"
:content="tooltipContent"
>
<span
ref="content"
:class="ellipsisClassRef.valueOf()"
:style="ellipsisStyleRef"
#click="handleClickRef"
>
<slot></slot>
</span>
</Tooltip>
</template>
setup(props, { slots }) {
onMounted(() => {
if (slots.default !== undefined) {
tooltipContent.value = slots.default()[0].children;
}
if (slots.tooltip !== undefined) {
// have to get the html content like below
tooltipContent.value = `<div style="color: blue">hello world</div>`;
}
});
}
So the main problem is how to convert the slot's content( html content) I got from parent, and turn it into the string and pass it to the wrap content. When I console.log(slots.tooltip), I know I can get the html content like a nested object like following,
0
:
{__v_isVNode: true, __v_skip: true, type: 'div', props: {…}, key: null, …}
length
:
1
[[Prototype]]
:
Array(0)
at
:
ƒ at()
length
:
1
name
:
"at"
arguments
:
(...)
caller
:
(...)
[[Prototype]]
:
ƒ ()
concat
:
ƒ concat()
constructor
:
ƒ Array()
copyWithin
:
ƒ copyWithin(
but there a lot of things to cover if I want to concat them into a string, so I wonder if there's a way to just get the html content as a string or like a helper function to call to get format like <div style="color:blue">Hello world</div>.

You can use v-html directive to return Raw HTML.
<span v-html="rawHtml"></span>
The other option could be Fallback Content in the slot, if the fallback content is static.
<button type="submit">
<slot>
Submit <!-- fallback content -->
</slot>
</button>
But if the slot content is dynamic, then you should better use the Render Functions
h('div', { style: { color: 'blue' }, innerHTML: 'hello world' })
Check my other answer here for details:
Adding custom HTML element inside template using functions Vue.js
Example
const { createApp, h } = Vue;
const myComponent = {
setup(props) {
return () => h('div', { style: { color: 'blue' }, innerHTML: 'hello world' })
}
}
const App = {
components: {
myComponent
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
<my-component></my-component><br/>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>

Related

getting slotProps from nested slot

I have a dynamic table. Table and table columns are separated components. And I am using columns as slot in table components.
main file
<DynamicTable :contents="contents">
// columns finding and displaying data from props dynamically. it works without a problem
<Col th="name" td="name"/>
<Col th="information" td="information"/>
// here I am giving custom slot inside last column, but button not displaying additionally I can't reach the content data via slotProps //
<Col>
<template #content="slotProps">
<button #click="getData(slotProps.content)">click me</button>
</template>
</Col>
</DynamicTable>
dynamicTable component
<slot></slot>
<div v-for="content in contents" :key="content.id">
<div v-for="(td, idx) in body" :key="idx">
// if there is slot given as template render <slot>
<slot v-if="td.children" name="content" :content=content></slot>
// else render dynamic value
<span v-else>{{ content[td.name] }}</span>
</div>
</div>
const slots = useSlots() ? useSlots().default : null;
const body = ref([])
slots().forEach(slot => {
body.value.push({
children: slot.children,
name: slot.props?.td,
})
})
additionally Col template
<script>
export default {
props: {
th: {
type: String,
default: null
},
td: {
type: String,
default: null
},
},
render(){
return null
}
}
</script>
In a situation like above, how can I display button element inside given <Col> component as slot and getting the :content="content" data as slotProps.
And if you need to know, content Array looks like below.
const contents = ref([
{
id: 341,
order_id: 1,
name: "susan",
information: "inf context",
},
{
id: 453,
order_id: 2,
name: "jack",
information: "info context",
}
])
Solved
You're trying to access slotProps in Col component,
But the Col component is not passing any props.
The required data is available in DynamicTable component
And you're already passing it to the slot of DynamicTable
You can access the slotProps as below
//App.vue
<DynamicTable :contents="contents" v-slot="slotProps">
<Col th="name" td="name" />
<Col>
Access slotProps directly, no need to use template
{{slotProps.content}}
</Col>
</DynamicTable>
And you need to remove slot name
//DynamicTable.vue
<slot v-if="td.children" :content="content"></slot>
Also refactor the Col component
//Col.vue
/* remove render function */
...
<template>
<slot />
</template>
Solution
You can find a working demo below.
https://stackblitz.com/edit/vue-cntmmb?file=src/App.vue

Angular Component with Content Projection Not Loading Properly in Storybook until a Control Is Changed

I'm new to Storybook, and am having trouble getting components which project ng-content to correctly receive input arguments on initial load.
Template:
<div id="alert-card">
<div>
<div class="iconDiv" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="10px">
<mat-icon *ngIf='this.alertType === "Warning"' class="warningIcon" aria-hidden="false" aria-label="Warning">warning</mat-icon>
<mat-icon *ngIf='this.alertType === "Error"' class="errorIcon" aria-hidden="false" aria-label="Error">error</mat-icon>
<mat-icon *ngIf='this.alertType === "Info"' class="infoIcon" aria-hidden="false" aria-label="Info">info</mat-icon>
<mat-icon *ngIf='this.alertType === "Confirm"' class="confirmIcon" aria-hidden="false" aria-label="Confirm">check_circle</mat-icon>
<span><span class="alertType">{{ alertKeyword }}</span>{{ alertMessage }} </span>
</div>
<ng-content select="[cardContent]"></ng-content>
</div>
</div>
Component:
import {
Component,
Input,
ViewEncapsulation,
} from '#angular/core';
#Component({
selector: 'alert-card',
templateUrl: './alert-card.component.html',
styleUrls: ['./alert-card.component.scss'],
encapsulation: ViewEncapsulation.None,
})
// Displays the alert card
export class AlertCardComponent {
// The type of alert
#Input()
alertType: AlertType;
// The keyword at the start of the alert message
#Input()
alertKeyword: string;
// The alert message to display
#Input()
alertMessage: string;
constructor() {}
}
// Indicates a type of alert which has an associated icon and color scheme in the CSS
export enum AlertType {
Warning = "Warning",
Error = "Error",
Info = "Info",
Confirm = "Confirm",
}
Story:
import { componentWrapperDecorator, Meta, Story } from '#storybook/angular';
import { AlertCardComponent, AlertType } from './alert-card.component';
export default {
component: AlertCardComponent,
decorators: [componentWrapperDecorator(AlertCardComponent)],
} as Meta;
const Template: Story = (args) => ({
props: args,
template: `
<div cardContent
style="padding:10px"
fxLayoutAlign="center">
Arbitrary HTML content can go in this area
</div>
`
});
export const Warning = Template.bind({});
Warning.args= {
alertType: AlertType.Warning,
alertKeyword: "Warning: ",
alertMessage: "something has happened!"
};
This is nearly working as expected, but within the story, the component loads without any of the input arguments:
If I change any of the Storybook controls, the input arguments are passed and the component displays as expected:
I feel like I'm missing something obvious. Everything works as expected once I start manipulating the controls within Storybook, but how do I get the input arguments passed on initial load?
I'm also having what I believe are related issues on Storybook's Docs page. The component is displayed as it would without any arguments passed regardless of how the inputs are controlled.

How do I properly deprecate Gutenberg block

I have a custom Gutenberg block (https://pastebin.com/bV2k5Ekc) in which I display text, link and an image. I want to change it so so instead of saving the image URL as a background image of the container, to use an img tag. Unfortunately - I can't manage to create the deprecation correctly - I fail at assigning the attributes parameters in the deprecation:
From this:
const {
attributes: {
mediaURL,
boxTitle,
boxDescription,
linkText,
linkHref,
linkTitle
},
className,
} = props;
let boxClass = 'cta-box';
let contentDescription = '';
if (boxDescription.length) {
boxClass += ' cta-box-description';
contentDescription = (
<p>
{boxDescription}
</p>
)
}
return (
<div className={`cta-block-box ${className}`}>
<a
className="cta-box-link"
href={linkHref}
style={{ backgroundImage: "url(" + mediaURL + ")" }}
rel="noopener noreferrer"
>
<div className={boxClass}>
<h3>
{boxTitle}
</h3>
{contentDescription}
<span className="arrow">{linkText ? linkText : linkTitle}</span>
</div>
</a>
</div>
);
},
To this (I only change what's in the return statement):
return (
<div className={`cta-block-box ${className}`}>
<a
className="cta-box-link"
rel="noopener noreferrer"
>
<img className="cta-box-image" src={linkHref} alt=""/>
<div className={boxClass}>
<h3>
{boxTitle}
</h3>
{contentDescription}
<span className="arrow">{linkText ? linkText : linkTitle}</span>
</div>
</a>
</div>
);
Which of course broke the Gutenberg element. So I added a deprecate to the blog, as much as I could following the official Wordpress documentation:
deprecated: [
{
attributes: {...this.attributes},
save: (props) => {
const {
attributes: {
mediaURL,
boxTitle,
boxDescription,
linkText,
linkHref,
linkTitle
},
className,
} = props;
console.log('dep');
console.log(props);
let boxClass = 'cta-box';
let contentDescription = '';
if (boxDescription.length) {
boxClass += ' cta-box-description';
contentDescription = (
<p>
{boxDescription}
</p>
)
}
return (
<div className={`cta-block-box ${className}`}>
<a
className="cta-box-link"
style={{ backgroundImage: "url(" + mediaURL + ")" }}
rel="noopener noreferrer"
>
<div className={boxClass}>
<h3>
{boxTitle}
</h3>
{contentDescription}
<span className="arrow">{linkText ? linkText : linkTitle}</span>
</div>
</a>
</div>
);
},
}
],
After this the editor page crashes, and I get error message in console, that attributes is not defined (displaying incorrect row in the script file). This is the "after" script contents (https://pastebin.com/dVdLMx7N).
react-dom.min.js?ver=16.13.1:125 ReferenceError: attributes is not defined
at save (cta-box2.js?f66a:242)
at Wt (blocks.min.js?ver=9ed25ffa009c799f99a4340915b6dc6a:3)
at Qt (blocks.min.js?ver=9ed25ffa009c799f99a4340915b6dc6a:3)
at block-editor.min.js?ver=4378547cec8f5157a02ead3dfc5c65b2:12
at hooks.min.js?ver=50e23bed88bcb9e6e14023e9961698c1:2
at $r (blocks.min.js?ver=9ed25ffa009c799f99a4340915b6dc6a:3)
at blocks.min.js?ver=9ed25ffa009c799f99a4340915b6dc6a:3
at Ur (blocks.min.js?ver=9ed25ffa009c799f99a4340915b6dc6a:3)
at blocks.min.js?ver=9ed25ffa009c799f99a4340915b6dc6a:3
at Array.reduce (<anonymous>)
Any help would be greatly appreciated! I suspect I'm missing some small detail, but so far I've failed to locate it. And was not able to find relevant enough information on the web.
Thanks in advance!
There are two issues in your "after" script:
The attributes do not match (and the this is actually the window object): attributes: {...this.attributes} (see line 212).
So what you used with the attributes property on line 24, should also be used with the same property on line 212. (because you only changed the output, so the block attributes remain the same)
The save output/markup also do not match — in the "before" script, you've got href={linkHref}, but in the deprecated property of the "after" script, the save output did not have that href. (see this diff)
So make sure the attributes and save output match the ones in the old/"before" script, and the following is how your code would look like, but note that I only included the main parts that need to be fixed:
// Define the attributes and use it with the root "attributes" property and
// the one in the "deprecated" property.
const blockAttributes = {
mediaID: {
type: 'number'
},
mediaURL: {
type: 'string'
},
boxTitle: {
type: 'string',
default: ''
},
// ... the rest of the attributes here.
};
registerBlockType('hswp/test-box', {
title: __('Test Box', 'modula'),
// ... your code.
attributes: blockAttributes,
// ... your code.
deprecated: [
{
attributes: blockAttributes,
save: (props) => {
// ... your code.
return (
<div className={`cta-block-box ${className}`}>
<a
className="cta-box-link"
href={linkHref}
style={{ backgroundImage: "url(" + mediaURL + ")" }}
rel="noopener noreferrer"
>
... your code.
</a>
</div>
);
},
}
],
});
Additionally, note that the PlainText component doesn't (as of writing) have a property named tagName.

How to use search in react js and get the result in particular div?

while searching, the results should appear as a div like below :
i use jquery to search in table,how to get the result like above.
my component code is:
<div id="modaldash" style={{ display: (searching ? 'block' : 'none') }}>
<p className="font-weight-medium" id="name"> <img id="logo" className="logo" src={jessica} alt="pam-logo" /> Jessica James </p>
<button id="Addlist" onClick={this.onSubmitdata} className="btn info">{this.state.shown ? "Addded" : "Add to list"}</button>
<p id="mailid">jessicajames#gmail.com </p>
<p id= "address">Mountain view,Ave</p>
</div>
its just a static content for css. how to use search and get results like above.
export default function App() {
// considering the data object to search on name
const [searchedData, setSearchedData] = useState([]);
const users = [
{
name: "abc1",
emailid: "abc1#gmail.com",
address: "23rd main, 2nd street"
},
{
name: "adb2",
emailid: "abc2#gmail.com",
address: "23rd main, 2nd street"
},
{
name: "adc3",
emailid: "abc3#gmail.com",
address: "23rd main, 2nd street"
}
];
const handleSearch = event => {
const data = users.filter(
user => user.name.indexOf(event.target.value) !== -1
);
setSearchedData(data);
};
const showSearchedData = () => {
return searchedData.map(user => (
<div key={user.emailid}>
<p className="font-weight-medium" id="name">
{" "}
<img id="logo" className="logo" src="" alt="pam-logo" />
{user.name}
</p>
<button id="Addlist"> Added/ add to list</button>
<p id="mailid">{user.emailid} </p>
<p id="address">{user.address}</p>
</div>
));
};
return (
<div className="App">
<input type="text" onChange={handleSearch} />
<div id="modaldash">{searchedData.length > 0 && showSearchedData()}</div>
</div>
);
}
You can add CSS to make a look and feel like shown in image attached.
Check the working example here https://codesandbox.io/s/falling-sun-r3rim

Pushing data to object in different component using POST

TL;DR I want to show submitted posts instantly instead of having to refresh my page
Using the Wordpress REST API I am able to create a new post without any issue. The post is being displayed as soon as the page refreshes, so what I want to do is update the posts object in my Hello.vue file as soon as I create that post so I don't need to refresh to show my newest posts.
I'm not really sure where to start - I've removed all of the experiments I've done so far (importing Post in Create, defining props, pushing to an array, reading about object reactivity on the official Vue documentation, nothing helped).
My App.js consists of the <router> object which shows Hello.vue and a component called Create which displays the Create.vue component. This is how my app currently looks like:
My App.vue file:
<template>
<div id="app">
<section class="posts">
<router-view></router-view>
<create></create>
</section>
</div>
</template>
<script>
import Create from '#/components/Create.vue'
export default {
name: 'app',
components: {
Create
}
}
</script>
<style lang="scss">
#import '../src/assets/styles/style.scss'
</style>
My Hello.vue which displays all the posts:
<template>
<div>
<section class="posts__Feed">
<ul class="posts__List">
<post v-for="item in posts" :item="item" :key="item.id"></post>
</ul>
</section>
</div>
</template>
<script>
var postsUrl = '/wp-json/wp/v2/posts/'
import Post from '#/components/Post.vue'
export default {
name: 'hello',
props: ['responseData'],
components: {
Post
},
data () {
return {
posts: []
}
},
beforeCreate () {
this.$http.get(postsUrl).then((response) => {
this.posts = response.data
})
}
}
</script>
And finally, the Create.vue file which creates the post:
<template>
<div>
<section class="posts__Create">
<form class="posts__CreateForm" v-on:submit="createPosts">
<div class="posts__CreateFormWrapper" v-bind:class="{ 'is-Loading': loading }">
<p>
<input v-model="formInfo.title" type="text" name="title" id="title" placeholder="Name" :disabled="formSent">
</p>
<p>
<textarea v-model="formInfo.content" name="content" id="content" cols="20" rows="10" maxlength="140" placeholder="Message" :disabled="formSent"></textarea>
</p>
<p>
<button :disabled="formSent">Send</button>
</p>
</div>
</form>
</section>
</div>
</template>
<script>
var postsUrl = '/wp-json/wp/v2/posts/'
export default {
name: 'create',
data () {
return {
formInfo: [],
responseData: [],
loading: false,
formSent: false
}
},
methods: {
createPosts (e) {
e.preventDefault()
var info = this.formInfo
// Check if fields are empty
if (this.formInfo.title && this.formInfo.content) {
this.loading = true
// POST
this.$http.post(postsUrl, info).then((response) => {
this.formSent = true
this.loading = false
// get body data
this.responseData = response.data
})
}
} // EOF createPosts
}
}
</script>
Any help would be much appreciated!
I ended up using an event bus as suggested by wotex. First, I've createad a file called bus.js with the below code:
import Vue from 'vue'
export const EventBus = new Vue()
Next, import bus.js to both .vue layouts using:
import { EventBus } from '#/bus.js'
Now emit the event as soon as a new post is created (this is sitting in my axios POST request inside the Create.vue file):
EventBus.$emit('newPost', this.responseData)
And finally, check if the event has happened on the other end (my Hello.vue file):
EventBus.$on('newPost', function (postData) {
Thanks for pointing me in the right direction!

Resources