How to remove extra wrapping elements in the rendered HTML? - javascript

I started learning angular 5 3 days ago so I'm quite new at it. I also use angularJS and React to develop applications and I think I don't understand how angular 5 components fully work. If I create for example a custom button that has a custom text inside (I'm not saying this should be done this way but it's a simple example that shows my point) like this:
<app-button>
<app-text>
My Text
</app-text>
</app-button>
The rendered DOM results in:
<app-button>
<button>
<app-text>
<span>
My Text
</span>
</app-text>
</button>
</app-button>
which is unreadable, I wanted to know if there's a way to remove this wrapping elements and just place the components layout replacing the tags resulting in the following structure:
<button>
<span>
My Text
</span>
</button>
If there's no way of removing them what are your suggestions? thanks!

Angular components are directives with templates. According to this:
Directive configuration #Directive({ property1: value1, ... })
selector: '.cool-button:not(a)' Specifies a CSS selector that
identifies this directive within a template. Supported selectors
include element, [attribute], .class, and :not().
So component selectors can be also attribute selectors. For your example, instead of writing this:
parent.component.html:
<app-button>
<app-text>
My Text
</app-text>
</app-button>
write this:
parent.component.html:
<button app-button>
<span app-text>My Text</span>
</button>
where :
app-button.component.ts
...
selector: '[app-button]',
template: `<ng-content></ng-content>
...
app-text.component.ts
...
selector: '[app-text]',
template: `<ng-content></ng-content>`
...
this would be rendered as you expected:
Update after your comment about styling those buttons:
To style the buttons from inside the button component, and set class in parent component, use :host-context pseudo-class. It is not deprecated and works well
button.component.css
:host-context(.button-1) {
background: red;
}
:host-context(.button-2) {
background: blue;
}
app.component.html
<button app-button class="button-1">
<span app-text>My Text</span>
</button>
<button app-button class="button-2">
<span app-text>My Text</span>
</button>
Here is the DEMO

I had a similar issue. I'll provide my solution in case someone else has the same problem.
My component should be able to be used either within other components or as a route from <router-outlet></router-outlet>. When I used the selector as an attribute [my-component] things worked perfectly provided it was used within other components. But when created by <router-outlet></router-outlet> a <div> were created automatically.
To avoid that, we can simply use multiple selectors, and consider that the selectors can be combined.
Consider this: I want my component to use the attribute my-component and if it ever should be created by the <router-outlet></router-outlet> it should be wrapped in a <section></section>. To achieve this simply use:
#Component(
selector: 'section[my-component], my-component',
...
)
The result will be, if used inside another tag:
<whatevertag my-component>
... component content ...
</whatertag>
If used as a route:
<section my-component>
... component content ...
</section>

Related

Is it possible to emulate v-model input behaviour on a paragraph tag?

Solution:
Thanks to #Jeronimas I figured out how to use dynamic components in vue. Basically you create a child component that switches between <p>and <input> tags, depending on props. You have to use <component> element for this, because it's an inherent vue component to manage dynamic components.
child:
<template>
<input v-if="input" type="text" value="hello world">
<p v-else>hello world</p>
</template>
<script setup>
const props = defineProps({
input: Boolean
})
//emit input when form is submitted...
</script>
parent:
<template>
<component :is="p" :input="true"/>
</template>
<script setup>
import p from './p.vue'
</script>
Vue is awesome <3.
Original Question:
Is it possible to input text into <p> tags, so that it will be stored in a database? I want to build a "live" CMS, similar to wordpress/webflow and using formatted <input> fields instead of <p>/<h> elements would make the html messy because, basically you would have to create two identical sites, one with normal text tags and the editable one with input fields, that look like normal text tags.
I was wondering if there is a way to manipulate reactive objects like ref to make it possible?
Alternatively you could create a site completely without normal text tags and instead use input field placeholders but that might mess up SEO.
Like in the comment above using :is could work when switching between the states.
<component :is="tag">
tag value could be dynamic and depending on the tag value the component would change its state.
I would suggest using div with v-html that supports any markup when in live mode and when user is in editing mode display a rich text field. For rich text editing i would suggest to look at https://tiptap.dev/
you can bind the input value and the tiptap would create any html tags you need <p/>, <li/>, <h1/>

Angular ngOnDestroy didn't fire

Demo
Demo fixed accordingly to accepted answer
Consider a component, let's call it <simple-dialog>, with this template:
<button type="button" (click)="visible = !visible">TOGGLE</button>
<div *ngIf="visible">
<ng-content select="[main]"></ng-content>
</div>
I omit the component TypeScript definition cause it's basically the same as the one generated by ng-cli.
Then I use it like this:
<simple-dialog>
<div main>
<app-form></app-form>
</div>
</simple-dialog>
When i first click the button the child component is rendered; if I click the button again the child component is removed from the DOM.
The problem is that, at this point, app-form's ngOnDestroy is not called.
I'm new to angular, so I am not sure whether my expectation is wrong.
What you are trying to achieve is called conditional content projection.
In your case, by using ng-content, the components are instantiated by the outer component, unconditionally - the inner component just decides not to display it.
If you want conditional instantiation, you should pass a template instead:
<simple-dialog>
<ng-template>
<div main>
<app-form></app-form>
</div>
</ng-template>
</simple-dialog>
and use an #ContentChild annotated property to access
the template from the content within the SimpleDialogComponent:
#ContentChild(TemplateRef, { static: false })
content!: TemplateRef<unknown>;
which can then be rendered in the template as
<div *ngIf="visible">
<ng-container [ngTemplateOutlet]="content"></ng-container>
</div>
You can also read about this here:
https://angular.io/guide/content-projection#conditional-content-projection

How to pass html template as props to Vue component

I have a textarea component that include html tag and I want to get html in edit mode in this component. I use Laravel to generate html.
<template>
<div>
<textarea
:value="content"
:name="name"
:id="id">
<slot></slot>
</textarea>
</div>
</template>
In blade page I used to this component:
<my-component>
<p class="textbox">hello world</p>
</my-component>
when I put this component in page show me tag <slot></slot> in textarea. What should I do? Do you have any solution for my need?
thanks
<textarea> components are treated as static by the Vue renderer, thus after they are put into the DOM, they don't change at all (so that's why if you inspect the DOM you'll see <slot></slot> inside your <textarea>).
But even it if they did change, that wouldn't help much. Just because HTML elements inside <textarea>s don't become their value. You have to set the value property of the TextArea element to make it work.
Anyway, don't despair. It is doable, all you need to overcome the issues above is to bring a small helper component into play.
There are many possible ways to achieve this, two shown below. They differ basically in how you would want your original component's template to be.
Solution: change <textarea> into <textarea-slot> component
Your component's template would now become:
<template>
<div>
<textarea-slot
v-model="myContent"
:name="name"
:id="id">
<slot></slot>
</textarea-slot>
</div>
</template>
As you can see, nothing but replacing <textarea> with <textarea-slot> changed. This is enough to overcome the static treatment Vue gives to <textarea>. The full implementation of <textarea-slot> is in the demo below.
Alternative solution: keep <textarea> but get <slot>'s HTML via <vnode-to-html> component
The solution is to create a helper component (named vnode-to-html below) that would convert your slot's VNodes into HTML strings. You could then set such HTML strings as the value of your <textarea>. Your component's template would now become:
<template>
<div>
<vnode-to-html :vnode="$slots.default" #html="valForMyTextArea = $event" />
<textarea
:value="valForMyTextArea"
:name="name"
:id="id">
</textarea>
</div>
</template>
In both alternatives...
The usage of the my-component stays the same:
<my-component>
<p class="textbox">hello world</p>
</my-component>
Full working demo:
Vue.component('my-component', {
props: ["content", "name", "id"],
template: `
<div>
<textarea-slot
v-model="myContent"
:name="name"
:id="id">
<slot></slot>
</textarea-slot>
<vnode-to-html :vnode="$slots.default" #html="valueForMyTextArea = $event" />
<textarea
:value="valueForMyTextArea"
:name="name"
:id="id">
</textarea>
</div>
`,
data() { return {valueForMyTextArea: '', myContent: null} }
});
Vue.component('textarea-slot', {
props: ["value", "name", "id"],
render: function(createElement) {
return createElement("textarea",
{attrs: {id: this.$props.id, name: this.$props.name}, on: {...this.$listeners, input: (e) => this.$emit('input', e.target.value)}, domProps: {"value": this.$props.value}},
[createElement("template", {ref: "slotHtmlRef"}, this.$slots.default)]
);
},
data() { return {defaultSlotHtml: null} },
mounted() {
this.$emit('input', [...this.$refs.slotHtmlRef.childNodes].map(n => n.outerHTML).join('\n'))
}
});
Vue.component('vnode-to-html', {
props: ['vnode'],
render(createElement) {
return createElement("template", [this.vnode]);
},
mounted() {
this.$emit('html', [...this.$el.childNodes].map(n => n.outerHTML).join('\n'));
}
});
new Vue({
el: '#app'
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<my-component>
<p class="textbox">hell
o world1</p>
<p class="textbox">hello world2</p>
</my-component>
</div>
Breakdown:
Vue parses the <slot>s into VNodes and makes them available in the this.$slots.SLOTNAME property. The default slot, naturally, goes in this.$slots.default.
So, in runtime, you have available to you what has been passed via <slot> (as VNodes in this.$slots.default). The challenge now becomes how to convert those VNodes to HTML String? This is a complicated, still open, issue, which may get a different solution in the future, but, even if it ever does, it will most likely take a while.
Both solutions above (template-slot and vnode-to-html) use Vue's render function to render the VNodes to the DOM, then picks up the rendered HTML.
Since the supplied slots may have arbitrary HTML, we render the VNodes into an HTML Template Element, which doesn't execute any <script> tags.
The difference between the two solutions is just how they "handle back" the HTML generated from the render function.
The vnode-to-html returns as an event that should be picked up by the parent (my-component) which uses the passed value to set a data property that will be set as :value of the textarea.
The textarea-slot declares itself a <textarea>, to the parent doesn't have to. It is a cleaner solution, but requires more care because you have to specify which properties you want to pass down to the <textarea> created inside textarea-slot.
Wrapping up and off-the-shelf alternatives
However possible, it is important to know that Vue, when parsing the declared <template> into <slot>s, will strip some formatting information, like whitespaces between top-level components. Similarly, it strips <script> tags (because they are unsafe). These are caveats inherent to any solutions using <slot>s (presented here or not). So be aware.
Typical rich text editors for Vue, work around this problem altogether by using v-model (or value) attributes to pass the code into the components.
Well known examples include:
vue-ace-editor: Demo/codepen here.
Vue Prism Editor: Demo here.
vue-monaco (the code editor that powers VS Code): demo here.
vue-codemirror: Demo here. This is by far the most starred on github.
They all have very good documentation in their websites (linked above), so it would be of little use for me to repeat them here, but just as an example, see how codemirror uses the value prop to pass the code:
<codemirror ref="myCm"
:value="code"
:options="cmOptions"
#ready="onCmReady"
#focus="onCmFocus"
#input="onCmCodeChange">
</codemirror>
So that's how they do it. Of course, if <slot>s - with its caveats - fit your use case, they can be used as well.
The short answer is NOT POSSIBLE
Your slot is put inside an textarea tag. Textare tag is only able to display the text content on its box.
So in the case you want a kind of "HTML edit mode", you may looking for an WYSIWYG editor, I recommend you can use CKEditor for VueJS, the editor even will allow you to direct edit HTML code
https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/frameworks/vuejs.html
Your HTML
<div id="app">
<ckeditor :editor="editor" v-model="editorData" :config="editorConfig"></ckeditor>
</div>
Your Component
const app = new Vue( {
el: '#app',
data: {
editor: ClassicEditor,
editorData: '<p>Editable Content HTML</p>',
editorConfig: {
// The configuration of the editor.
}
}
} );
In your case if you want to write your own content editor you can use div with attribute contenteditable="true" rather than textarea. After this you can write your text decoration methods ...
The generated html with laravel store in myhtml and use it in vue component.
Example: I also uploaded to codesandbox [Simple Vue Editor]
<template>
<div>
<button #click="getEditorCotent">Get Content</button>
<button #click="setBold">Bold</button>
<button #click="setItalic">Italic</button>
<button #click="setUnderline">Underline</button>
<button #click="setContent">Clear</button>
<div class="myeditor" ref="myeditor" contenteditable v-html="myhtml" #input="onInput"></div>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String
},
data: () => {
return {
myhtml:
"<h1>Simple editor</h1><p style='color:red'>in vue</p><p>Hello world</p>" // from laravel server via axios call
};
},
methods: {
onInput(e) {
// handle user input
// e.target.innerHTML
},
getEditorCotent() {
console.log(this.$refs.myeditor.innerHTML);
},
setBold() {
document.execCommand("bold");
},
setItalic() {
document.execCommand("italic");
},
setUnderline() {
document.execCommand("underline");
},
setContent() {
// that way set your html content
this.myhtml = "<b>You cleared the editor content</b>";
}
// PS. Good luck!
}
};
</script>
<style scoped>
.myeditor {
/* text-align: left; */
border: 2px solid gray;
padding: 5px;
margin: 20px;
width: 100%;
min-height: 50px;
}
</style>

Is it possible to use ngFor to display multiple component selector tags?

I have a question in angular 2.
so i am able to use ngFor to display {{data}} tags from a local json and display the data.
However i was wondering if theres a way to render component selector tags that are stored in a json and display them within an ng for. for example,
I have this json component.ts in my file: (See comments for explanation about the issue)
import { Component, OnInit } from '#angular/core';
import * as jQuery from 'jquery';
#Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit {
widgets;
constructor() { }
ngOnInit() {
this.widgets = [
{
"widgetcode": "<app-analytics-widget></app-club-chooser>" //Existing component selector tag within the angular cli system
},
{
"widgetcode": "<app-club-chooser></app-club-chooser>" //Existing component selector tag within the angular cli system
},
{
"widgetcode": "<app-account-widget></app-account-widget>" //Existing component selector tag within the angular cli system
}
]
}
}
my component.html file:
<div class="DahsboardWrapper">
<!-- DashBoard Ges here -->
<!-- widget elements go here -->
<div class="DashoardHeaderClass" id="dashboard">
Dashboard Loaded
<div class="WidgetAreaWrapper">
<div class="widgets" *ngFor="let widget of widgets">
<!-- {{widget.widgetcode}} Not working atm..-->
<!-- this is displayng just plain text -->
</div>
<!-- This is working -->
<app-analytics-widget></app-analytics-widget>
<app-club-chooser></app-club-chooser>
<app-account-widget></app-account-widget>
</div>
</div>
Any suggestions maybe theres a better way to do this?
You cannot go from selector. That wouldn't work in production with Ahead of Time (AoT) compiler anyway, because the compiler runs at build time, and it is what matches selectors to components and directives.
If you have a limited set of tags in mind, you can use ngSwitch. Each of the components you support would be one of the switch cases, and you choose which one to display by some string property in your JSON. The entire [ngSwitch] would be the ngFor item.
If you need to support more complex scenarios, and you are using Angular 4, you can use ngComponentOutlet'. This still required a Type (importing the class) not a selector.
Finally, also Angular 4, for dynamically loading components in general, you can pick which way makes most sense to you by the many ways supported in this library https://github.com/apoterenko/ngx-dynamic-template.

Angular 2 make function available in ng-content

I have a dropdown with a template like this:
<div class="dropdown-trigger" (click)="contentToggle()">
<ng-content select="dropdown-trigger"></ng-content>
</div>
<div class="dropdown-content" *ngIf="showContent">
<ng-content select="dropdown-content"></ng-content>
</div>
I'd like to be able to use contentToggle() in whatever I put in the ng-content so that I can have additional elements that can be used to close the dropdown, for example I might want a close button... What's the best way to do this?
This will do the trick:
<dropdown #dropdown>
<button dropdownTrigger (click)="dropdown.toggleDropdown()">Click me</button>
</dropdown>
You just assign a local template variable to the component which gives you access to everything the component has in it. Including the function you want to call.
Note that you should/need to also change the select bits to this:
<ng-content select="[dropdownTrigger]"></ng-content>
<ng-content select="[dropdownContent]"></ng-content>
Angular allow you to do this trick, Example:
import { Component } from '#angular/core'
#Component(){...}
export class DropdownComponent {
toggleDropdown() {...}
}
//parent.component.html
<dropdown-content #myDropdown>
<a (click)="myDropdown.toggleDropdown()">Close the dropdown</a>
</dropdown-content>
If you want to get a callback to the event I recommend you to read the Output Decorator

Categories