Curious case of "Unexpected closing tag" in modern Angular [closed] - javascript

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 2 years ago.
Improve this question
Suppose we have a data model, a list of objects that represent text items to be rendered in template. Each carry a text and a type. A type could be HEADER, PARAGRAPH and LIST. Each would correspond to a similar HTML tag, so HEADER would be rendered by <h1>, PARAGRAPH would be <p> and LIST would be a <li> eventually wrapped in an <ul></ul>. We have a given set of these objects. For example:
[
{ textType: 'HEADER', text: 'Welcome!' },
{ textType: 'PARAGRAPH', text: 'Lorem ipsum is a lie, here are the facts:' },
{ textType: 'LIST', text: 'List item #1' },
{ textType: 'LIST', text: 'List item #2' },
{ textType: 'LIST', text: 'List item #3' },
{ textType: 'HEADER', text: 'Welcome again!' },
{ textType: 'PARAGRAPH', text: 'Lorem ipsum is not a lie after all, counterpoints:' },
{ textType: 'LIST', text: 'Another List item #1' },
{ textType: 'LIST', text: 'Another List item #2' },
{ textType: 'LIST', text: 'Another List item #3' },
{ textType: 'PARAGRAPH', text: 'That\'s all, folks!' }
]
The problem is quite obvious - both HEADER AND PARAGRAPH can be expressed by a one closed tag with HTML content. But LIST not only requires a standard <li> element, but also a <ul> (if we assume want strict HTML). How to render this within Angular template? I went about a most natural approach I could think of - *ngFor with some additional, unusual albeit reasonable logic added. In a way, it looks like an old-school PHP-style render.
<ng-container *ngFor="let textItem of summaryText; let index = index">
<h1 *ngIf="textItem.textType === 'HEADER'">{{ textItem.text }}</h1>
<p *ngIf="textItem.textType === 'PARAGRAPH'">{{ textItem.text }}</p>
<ng-container *ngIf="textItem.textType ==='LIST'">
<ng-container *ngIf="(index === 0) || (summaryText[index-1] && (summaryText[index-1].textType !== 'LIST'))">
<ul>
</ng-container>
<li>{{ textItem.text }}</li>
<ng-container *ngIf="(index === (summaryText.length - 1)) || (summaryText[index+1] && (summaryText[index+1].textType !== 'LIST'))">
</ul>
</ng-container>
</ng-container>
</ng-container>
I was somewhat surprised to learn, that this wouldn't compile. It results with Unexpected closing tag "ng-container". And, sure enough, when you think about it, that makes sense. Angular enforces proper HTML so it expects the <ul> closure within a directive, when there's none. I understand that well enough.
But that still leaves me with an unsolved problem - how to render the list wrapped in <ul>, when I don't know when it will happen in that initial model? I cannot assume <ul> anywhere. I cannot even assume the order of the items in that model - it might as well just be a PARAGRAPH, LIST, PARAGRAPH, LIST and what not (it would result in two lists with single item each).
Is there any way to achieve that without influencing the data model? I know I could theoretically change the model, make a separate list inside, detect that, start ul and so on. But in this case, for some reason (please assume it's reasonable and that I know what I'm talking about) I cannot change the model.
I was wondering if there's any way to make Angular render that properly. Or, if this is - perhaps - a wrong approach, there is another way to tackle this directly in the template?

I created a bug-fixed version here for you on Stackblitz.
The error is because of that the HTML Nested tag rules are violated here in your template:
<ng-container *ngIf="(index === 0) || (summaryText[index-1] && (summaryText[index-1].textType !== 'LIST'))">
<ul>
</ng-container>
You have a closing tag </ng-container> before closing inner tag <ul>.
In addition You have to change the JSON object data for have better structure of <li> items like this:
this.summaryText = [
{ textType: 'HEADER', text: 'Welcome!' },
{ textType: 'PARAGRAPH', text: 'Lorem ipsum is a lie, here are the facts:' },
{ textType: 'LIST', text: ['List item #1', 'List item #2', 'List item #3'] },
{ textType: 'HEADER', text: 'Welcome again!' },
{ textType: 'PARAGRAPH', text: 'Lorem ipsum is not a lie after all, counterpoints:' },
{ textType: 'LIST', text: ['Another List item #1', 'Another List item #2', 'Another List item #3'] },
{ textType: 'PARAGRAPH', text: 'That\'s all, folks!' }
]

Related

How can I use multiple b-form-radio-group avoiding visual interference among them?

I'm new using Vue and specifically Bootstrap Vue and I'm trying to build a form with multiple radio groups.
My problem is that when I change the value in one of them the others don't change their values (this was checked with Vue DevTools) but visually it looks like none of the values are selected
I don't know what is wrong with my approach
I post here a simplified version of the code looking for some help, thanks in advance:
<template>
<div>
<b-form-group label="Radio group 1" v-slot="{ ariaDescribedby }">
<b-form-radio-group
id="radio-group-1"
v-model="selected1"
:options="options1"
:aria-describedby="ariaDescribedby"
name="radio-options"
></b-form-radio-group>
</b-form-group>
<b-form-group label="Radio Group 2" v-slot="{ ariaDescribedby }">
<b-form-radio-group
id="radio-group-2"
v-model="selected2"
:options="options2"
:aria-describedby="ariaDescribedby"
name="radio-options"
></b-form-radio-group>
</b-form-group>
</div>
</template>
<script>
export default {
data() {
return {
selected1: 'first',
options1: [
{ text: 'First', value: 'first' },
{ text: 'Second', value: 'second' },
],
selected2: 'one',
options2: [
{ text: 'One', value: 'one' },
{ text: 'Two', value: 'two' },
]
}
}
}
</script>
Since both <b-form-radio-group> elements have the same name, "radio-options", visually they are treated like one group. The different v-model would still function correctly but this isn't what you want visually. Give the second group a different name:
<b-form-radio-group
id="radio-group-2"
v-model="selected2"
:options="options2"
:aria-describedby="ariaDescribedby"
name="radio-options2"
></b-form-radio-group>
Here I changed it to radio-options2.

How can I change the value of an item in an array based on multiple conditions?

I am trying to change the value of an item in array, based on matching other items in the array. The array might contain details of the section (non-unique), a unique ID, and a value I wish to change (in this case a 'selected' flag). The goal is to be able to have multiple items which can have their selected flag. Within any single section, only one item could be 'selected' but multiple sections could have an individual item 'selected'. Conceptually, I think this could be thought of in the same way as having multiple groups of radio buttons.
The ultimate aim is to be able to use state to remember the selections made in a component that is created using props. I'm keen to understand not simply copy. I'll get my head around state mutations next, but better to solve this problem first.
So, take an array like:
menuItems: [
{
section: 'National',
id: 'First item',
selected: false
},
{
section: 'National',
id: 'Second item',
selected: false
},
{
section: 'National',
id: 'Third item',
selected: true
},
{
section: 'Local',
id: 'Fourth item',
selected: false
},
{
section: 'Local',
id: 'Fifth item',
selected: false
},
{
section: 'Local',
id: 'Sixth item',
selected: true
}
]
And some search strings like:
searchSection: 'National',
searchId: 'First item'
How would I create a function that could change the selected flag of the item with id: First item to true, the others (second, third item) to false, and don't change anything in the 'Local' section?
I have tried to get my head around using forEach loops to no avail, even though this feels the right approach. Using findIndex for the section seems destined to fail as there are multiple items to be found.
First SO question - so pre-emptive apologies if problems with the way I have asked. I'm using Vue3. All advice appreciated.
Loop through the items testing for the proper section. With the section, if there is an id match, set selected to true, otherwise set selected to false:
methods: {
flag(searchSection, searchId) {
this.menuItems.forEach(item => {
if (item.section === searchSection) {
item.selected = item.id === searchId;
}
});
}
}
Call the function:
this.flag('National', 'First item');

TinyMCE Plugin Template add category Listbox

I'm developping for my entreprise an external template manager with ASP.net.
in this external template manager, users can execute all the CRUD actions, so they can create/ read /update /delete.
Many departements of my entreprise use thoses templates, so I've created a category fields in my template manager, so they can filter a bit.
I'would like to modify, the plugin.js of the template plugin to add a dynamic listbox category.
So when a user want to add a template in the editor he can select his departements[category] and only the related template will be shown in the listbox [template].
I get all the template definition[title, content, description, category] from a MS SQL DB with an ASP repeater Control.
the result of the repeater generate the definition needed by the template plugin
templates:
[
{"title": "Some title 1", "description": "Some desc 1", "content": "My
content", "category": "support"},
]
So i've tried just to test (because i'm not good with JS ad Oriented Object) :
`
win = editor.windowManager.open({
title: 'Insert template',
layout: 'flex',
direction: 'column',
align: 'stretch',
padding: 15,
spacing: 10,
items: [
{
type: 'form', flex: 0, padding: 0, items: [
{
type: 'container', label: 'Templates', items: {
type: 'listbox', label: 'Templates', name: 'template',
values: values, onselect: onSelectTemplate
}
//Added BY ME
type: 'container', label: 'Category', items: {
type: 'listbox', label: 'Category', name:'category', values:
valuesCategory, onselect: onSelectCategory
}
}
]
},
{ type: 'label', name: 'description', label: 'Description', text:
'\u00a0' },
{ type: 'iframe', flex: 1, border: 1 }
],
onsubmit: function () {
insertTemplate(false, templateHtml);
},
minWidth: Math.min(DOMUtils.DOM.getViewPort().w,
editor.getParam('template_popup_width', 600)),
minHeight: Math.min(DOMUtils.DOM.getViewPort().h,
editor.getParam('template_popup_height', 500))
});
win.find('listbox')[0].fire('select');
win.find('listbox')[0].fire('select');
}`
and defined valuesCategory & onSelectCategory somewhere over that.
the list box isn't showed in the Dialog ?
I've searched on internet to find something similar with tinymce but i don't find anything like this, could someone help me to develop this functionnality ?
Thanks for your answers !
Have a nice day
Instead of modifying the editor's source code why not calculate the valid plugins from the server side and only include the relevant templates in the editor configuration?
This eliminates the need for you to modify the source code of the editor and people will not have the ability to see templates for a different department.

ExtJs - How to fetch elements from childEls

I'm using the snippet bellow to add some HTML elements to the top of a FormPanel:
{
xtype: 'component',
itemId: 'form-top',
cls: 'form-top-items',
renderTpl: [
'<div id="{id}-formHelp" class="form-help">',
'<span class="form-help-text">{helpText}</span>',
'</div>'
],
renderData: {
helpText: __("Os campos com * são de preenchimento obrigatório.")
},
childEls: [
{name: 'formHelp', itemId: 'form-help'}
]
}
But once the component is rendered, I can't fetch any child items.
I'm expecting a way to access the formHelp item somehow, but can't find it anyway bellow the form-top component.
The itemId must match the portion of your child el's id that comes after {id}-.
You have two options:
Change your itemId to 'formHelp'
OR
change your div to <div id="{id}-form-help" class="form-help">

Dojo datagrid: sort by first value in cell layout

I have used a formatter to add some html (a link, a break and a span) on or around different values. The issue now is that the column does not sort. I think it is because every cell in the column starts with the same value (a link with the same url. Eventually the urls will be different).
Here is the data:
{id: 1, 'main': 'puma', 'description': 'A puma is a cat', 'url': 'http://www.google.com', 'filesize': '12.34', date: '2010-01-01'},
{id: 2, 'main': 'tiger', 'description': 'Tiger, another cat', 'url': 'http://www.google.com', 'filesize': '43.27', date: '2013-03-04'},
{id: 3, 'main': 'Wombat', 'description': 'wombat, not a cat', 'url': 'http://www.google.com', 'filesize': '59.01', date: '2011-03-08'}
This is for the layout (left out the other columns from this sample):
{name: 'Title', fields: ['main','description','url','filesize'], 'width': '200px',formatter: formatLink}
And this is the formatter:
function formatLink(value){
return ''+value[0]+'<br />'+value[1]+'<span class="smalltxt"> File Size: ' + value[3] + 'MB</span>';
}
For clarity, to see it all in action: http://jsfiddle.net/QXYDK/6/
Ideally I would not have all this stuff in the same cell, but it required for this project.
The center column does not sort at all at the moment. Is there a way I can choose to sort by value[0] (the first field, 'main')?
I don't know if it's a bug in Dojo or not, but Dojo only defines sorting on the field attribute. But because you're using a custom field with a formatter, you only use fields and not field.
The solution is to define the field you want to sort on (in this case "main").
The code for your layout would become:
var layout = [
{name: 'Index', field: 'id'},
{name: 'Title', fields: ['main','description','url','filesize'],field: 'main', 'width': '200px',formatter: formatLink},
{name: 'Date', field: 'date', width: 10, formatter: formatDate}
];
By default, Dojo sorts your data case sensitive, which means that Wombat (which starts with a capital) will be sorted differently than puma and tiger (which start with a lower case letter). To enable case insensitive sorting, you need to define a comparatorMap on your store, for example:
store.comparatorMap = new Object();
store.comparatorMap["main"] = function(a, b) {
if (a.toLowerCase() < b.toLowerCase()) {
return -1;
} else if (a.toLowerCase() == b.toLowerCase()) {
return 0;
} else {
return 1;
}
};
I also updated your JSFiddle with this new information. The result can be found here.

Categories

Resources