Good day. I'm using Vue.js to render an arbitrary number of select elements from the data in a component.
Here's sample JSON data that indicates there are two select elements, each with one or more options.
{
"dropdowns":[
{
"cd":"UG9ydGZvbGlv",
"formname":"sp_filter_UG9ydGZvbGlv",
"nm":"Portfolio",
"selected":"1a",
"options":[
{
"cd":"1a",
"val":"Option 1A"
}
]
},
{
"cd":"UHJvZHVjdCBOYW1l",
"formname":"sp_filter_UHJvZHVjdCBOYW1l",
"nm":"Product Name",
"selected":"2b",
"options":[
{
"cd":"2a",
"val":"Option 2A"
},
{
"cd":"2b",
"val":"Option 2B"
}
]
}
]
}
Here's the template HTML:
<form>
<div v-for="dropdown in dropdowns">
<div v-if="dropdown.availableToView">
<h4>{{dropdown.nm}}</h4>
<select v-model="dropdown.selected" v-on:change="triggerUpdate">
<option value="">(Make a selection)</option>
<option v-for="option in dropdown.options" :value="option.cd">{{option.val}}</option>
</select>
</div>
</div>
</form>
So far so good.
I've got the data loading and Vue is building the dropdowns.
When the user changes any select box (remember there can be an arbitrary number of them), the trigger action needs to submit ALL of the elements in the form via ajax. It sounds like the most correct option is to bind the form fields to the underlying component data, as I've done.
My triggerUpdate looks like this:
methods: {
triggerUpdate: function() {
axios({
method: "post",
url: actionURL,
data: this.dropdowns
})
.then(response => (this.data = response));
}
}
...but this submits the entire dropdowns data element, including all of the options in each select box. It's unnecessary to send all of the options in. I just want to send each field name along with its selected option (i.e. the "value").
I know i could serialize the whole form and make that my ajax payload. But that seems to be making an "end run" around Vue.js. Everyone talks about having your form fields bound to the Vue model...is it then correct to basically ignore the model when making an ajax request whose purpose is to then update the model?
I'm relatively new to Vue.js so I'd appreciate help with what I'm overlooking here. How should I go about sending in the data from the form (a) while using proper Vue.js binding and (b) without sending extraneous data?
Thanks for your time.
If you need to post only the selected values, and you store those in each dropdown's selected property, the sensible approach seems to be just mapping it to a simple array of name/value objects.
Try this (it assumes the name of each field is the formname property, if it isn't you can just replace it):
var submitData = this.dropdowns.map((dropdown) => {
return { name: dropdown.formname, value: dropdown.selected };
});
Then you send submitData in your ajax request.
I have an issue with complex object reactivity.
I've read everything I can on stack to find a way to solve it, but nothing works. I've looked at object reactvity and array caveats on vuejs, but not working either.
So I'm asking some help please.
Let me explain the project:
I have 2 columns :
- on the left side, I CRUD my content
- on the right side, I display the results
I have my object, and I'm adding new elements on its "blocks" property (text, images, etc...)
[
{
"uid": 1573224607087,
"animation": "animationName",
"background": {
"bckColor": "#ff55ee",
...
},
"blocks": []
}
]
On click event, I add a new element via this method. Everything is ok, I can CRUD a block.
addBloc(el) {
if (el.type == "text") {
const datasA = {
type: "text",
uid: Date.now(),
slideId: this.pagination.currentPage,
content: el.content,
css: {
color: "#373737",
...
},
...
};
this.slides[this.pagination.currentPage].blocks.push(datasA);
this.$bus.$emit("newElement", datasA);
}
To modify the order of my elements on the display side, I added a drag and drop module to move my block on my DOM tree. Smooth dnd
The problem is, when I drang&drop my element, my object is updated correctly, but the DOM isn't. The dragged element goes back to its initial position.
What is strange, when I try to modify my block (the one I dragged), it modifies the other one.
I'me adding a small video, so you can see what's happening.
Small animation to show you what's going on
I add some more explainations.
I use event bus to communicate between my components, and the right side is using its own object!
I don't know how I can solve this issue.
Tell me if you need more information.
Thank you all !
EDIT 1 :
I added an id to each block to see what happens when I start Drag&Drop. ==> blocks are moving correctly. The problem is not coming from the method onDrop() but from my nested components if I understand well. They don't update. I'm going to search for this new issue.
I've added a new gif to show what's going on.
This is the nested structure
TheSidebar.vue => top container
<Container
:data-index="i"
#drop="onDrop(i,$event)"
:get-child-payload="itemIndex => getChildPayload(i, itemIndex)"
lock-axis="y"
>
<Draggable
v-show="pagination.currentPage === i"
v-for="(input, index) in slides[i].blocks"
:key="index.uid"
:id="'slideBlocksContainer'+index"
class="item"
>
blockId #{{input.uid}}
<AppContainer
v-if="input.type == 'text'"
:blocType="input.type"
:placeholder="input.content"
:id="index"
:slideId="i"
></AppContainer>
</Draggable>
</Container>
Then I have my AppContainer.vue file, which is a top level. In this I have the specific elements of each input type
And I have AppElement.vue file, which is common elements, I can use everywhere
Something like this
TheSidebar
--AppContainer
----AppElement
Know I don't know yet, how to force vue to update AppContainer.vue and AppElement.vue
EDIT 2 :
As suggested in this article I've changed the key of the component and now , when I drag and drop my elements, they stay where they are dropped.
What I see also, is that the AppElement inputs, are related to their own AppContainer. So everything is ok now, but I don't know if it is best practices.
The issue appears to be that the Smooth dnd library you are using is not updating the array of blocks that you are passing to it, it is likely making a copy of the array internally. So when you change the position of the blocks by dragging and dropping, you are not changing your blocks array, just the internal copy.
Looking at the Smooth dnd documentation, if you wanted to access the modified array you could try using the drag-end event handler:
onDragEnd (dragResult) {
const { isSource, payload, willAcceptDrop } = dragResult
}
I have create my own plugin for link. Now I want to add some other attributes to a tag generated by the plugin, like target, rel.
But I am not able to get it done. Here is the my plugins code for converter.
What converters I should add so that a tag can support other attributes?
/**
* #license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
/**
* #module link/linkediting
*/
import LinkEditing from '#ckeditor/ckeditor5-link/src/linkediting';
import {
downcastAttributeToElement
} from '#ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import { upcastElementToAttribute } from '#ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
import LinkCommand from './uclinkcommand';
import UnlinkCommand from './ucunlinkcommand';
import { createLinkElement } from '#ckeditor/ckeditor5-link/src/utils';
import { ensureSafeUrl } from './utils';
import bindTwoStepCaretToAttribute from '#ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute';
/**
* The link engine feature.
*
* It introduces the `linkHref="url"` attribute in the model which renders to the view as a `<a href="url">` element.
*
* #extends module:core/plugin~Plugin
*/
export default class UcLinkEditing extends LinkEditing {
/**
* #inheritDoc
*/
init() {
const editor = this.editor;
// Allow link attribute on all inline nodes.
editor.model.schema.extend( '$text', { allowAttributes: 'linkHref' } );
editor.conversion.for( 'dataDowncast' )
.add( downcastAttributeToElement( { model: 'linkHref', view: createLinkElement } ) );
editor.conversion.for( 'editingDowncast' )
.add( downcastAttributeToElement( { model: 'linkHref', view: ( href, writer ) => {
return createLinkElement( ensureSafeUrl( href ), writer );
} } ) );
editor.conversion.for( 'upcast' )
.add( upcastElementToAttribute( {
view: {
name: 'a',
attribute: {
href: true
}
},
model: {
key: 'linkHref',
value: viewElement => viewElement.getAttribute( 'href' )
}
} ) );
// Create linking commands.
editor.commands.add( 'ucLink', new LinkCommand( editor ) );
editor.commands.add( 'ucUnlink', new UnlinkCommand( editor ) );
// Enable two-step caret movement for `linkHref` attribute.
bindTwoStepCaretToAttribute( editor.editing.view, editor.model, this, 'linkHref' );
// Setup highlight over selected link.
this._setupLinkHighlight();
}
}
Introduction
Before I get to the code I'd like to take the occasion to explain the CKEditor 5 approach to inline elements (like <a>) so that the solution is easier to understand. With that knowledge, similar problems in the future should not be troubling. Following is meant to be a comprehensive tutorial, so expect a long read.
Even though you may know most of the things in the theory part, I recommend reading it to get the full understanding of how things work in CKEditor 5.
Also, do note that I will present a solution for the original CKEditor 5 plugin as it will be more valuable to other community members seeking a tutorial on this matter. Still, I hope that with the insight from this tutorial you will be able to adjust the code sample to your custom plugin.
Also, keep in mind that this tutorial does not discuss UI part of this plugin, only how things should be configured for conversion purposes. Adding and removing attributes is the job for UI or for some other part of code. Here I discuss only engine stuff.
Inline elements in CKEditor 5
First, let's establish which elements are inline. By inline elements I understand elements like <strong>, <a> or <span>. Unlike <p>, <blockquote> or <div>, inline elements do not structure the data. Instead, they mark some text in a specific (visual and semantical) way. So, in a way, these elements are a characteristic of a given part of a text. As a result, we say that given part of a text is bold, or that given part of a text is/has a link.
Similarly, in the model, we don't represent <a> or <strong> directly as elements. Instead, we allow adding attributes to a part of a text. This is how text characteristics (as bold, italic or link) are represented.
For example, in the model, we might have a <paragraph> element with Foo bar text, where bar has the bold attribute set to true. We would note it this way: <paragraph>Foo <$text bold="true">bar</$text></paragraph>. See, that there is no <strong> or any other additional element there. It's just some text with an attribute. Later, the bold attribute is converted to <strong> element.
By the way: view elements that come from model attributes have their own class: view.AttributeElement and instead of inline elements can be also called attribute elements. Sadly, the name conflicts with "attribute" as an attribute of a view element (what is worse, attribute element is allowed to have attributes).
Of course, text may have multiple attributes and all of them are converted to their respective view inline elements. Keep in mind that in the model, attributes do not have any set order. This is contrary to the view or HTML, where inline elements are nested one in another. The nesting happens during conversion from the model to the view. This makes working in model simpler, as features do not need to take care of breaking or rearranging elements in the model.
Consider this model string:
<paragraph>
<$text bold="true">Foo </$text>
<$text bold="true" linkHref="bar.html">bar</$text>
<$text bold="true"> baz</$text>
</paragraph>
It is a bold Foo bar baz text with a link on bar. During conversion, it will be converted to:
<p>
<strong>Foo </strong><strong>bar</strong><strong> baz</strong>
</p>
Note, that the <a> element is converted in a way that it is always the topmost element. This is intentional so that none element will ever break an <a> element. See this, incorrect view/HTML string:
<p>
Foo <strong>bar</strong>
</p>
The generated view/HTML has two link elements next to each other, which is wrong.
We use priority property of view.AttributeElement to define which element should be on top of others. Most elements, like <strong> do not care about it and keep the default priority. However, <a> element has changed priority to guarantee a proper order in the view/HTML.
Complex inline elements and merging
So far we mostly discussed the simpler inline elements, i.e. elements which don't have attributes. Examples are <strong>, <em>. In contrary, <a> has additional attributes.
It is easy to come up with features that need to mark/style a part of a text but are custom enough so that simply using a tag is not enough. An example would be a font family feature. When used, it adds fontFamily attribute to a text, which is later converted to <span> element with an appropriate style attribute.
At this point, you need to ask what should happen if multiple such attributes are set on the same part of a text? Take this model example:
<paragraph>
<$text fontFamily="Tahoma" fontSize="big">Foo</$text>
</paragraph>
The above attributes convert as follow:
fontFamily="value" converts to <span style="font-family: value;">,
fontSize="value" converts to <span class="text-value">.
So, what kind of view/HTML could we expect?
<p>
<span style="font-family: Tahoma;">
<span class="text-big">Foo</span>
</span>
</p>
This, however, seems wrong. Why not have just one <span> element? Wouldn't it be better this way?
<p>
<span style="font-family: Tahoma;" class="text-big">Foo</span>
</p>
To solve situations like these, in CKEditor 5 conversion mechanism we, in fact, introduced a merging mechanism.
In the above scenario, we have two attributes that convert to <span>. When the first attribute (say, fontFamily is converted, there is no <span> in the view yet. So the <span> is added with the style attribute. However, when fontSize is converted, there is already <span> in the view. view.Writer recognizes this and checks whether those elements can be merged. The rules are three:
elements must have the same view.Element#name,
elements must have the same view.AttributeElement#priority,
neither element may have view.AttributeElement#id set.
We haven't discussed id property yet but, for simplicity reasons, I won't talk about it now. It is enough to say that it is important for some attribute elements to prevent merging them.
Adding another attribute to the link
At this point, it should be pretty clear how to add another attribute to <a> element.
All that needs to be done is defining a new model attribute (linkTarget or linkRel) and make it convert to <a> element with the desired (target="..." or rel="...") attribute. Then, it will be merged with the original <a href="..."> element.
Keep in mind that <a> element from the original CKEditor 5 link plugin has custom priority specified. This means that the element generated by the new plugin need to have the same priority specified to be properly merged.
Upcasting merged attribute elements
For now, we only discussed downcasting (i.e. converting from the model to the view). Now let's talk about upcasting (i.e. converting from the view to the model). Fortunately, it is easier than the previous part.
There are two "things" that can be upcasted - elements and attributes. No magic here - elements are elements (<p>, <a>, <strong>, etc.) and attributes are attributes (class="", href="", etc.).
Elements can be upcast to elements (<p> -> <paragraph>) or attributes (<strong> -> bold, <a> -> linkHref). Attributes can be upcast to attributes.
Our example clearly needs upcasting from an element to an attribute. Indeed, <a> element is converted to linkHref attribute and the linkHref attribute value is taken from href="" attribute of the <a> element.
Naturally, one would define the same conversion for their new linkTarget or linkRel attribute. However, there is a trap here. Each part of the view can be converted ("consumed") only once (this is also true for the model when downcasting).
What does it mean? Simply, if one feature already converted given element name or given element attribute, neither feature can also convert it. This way features can correctly overwrite each other. This also means that general-purpose converters can be introduced (for example, <div> can be converted to <paragraph> if no other feature recognized <div> as something that can be converted by that feature). This also helps to spot conflicting converters.
Back to our example. We cannot define two element-to-attribute converters that convert the same element (<a>) and expect them to work together at the same time. One will overwrite the other.
Since we don't want to change the original link plugin, we need to keep that converter as is. However, the upcast converter for the new plugin will be an attribute-to-attribute converter. Since that converter won't convert element (or rather, element name) it will work together with the original converter.
Code sample
Here is a code sample for a link target plugin. Below I will explain some parts of it.
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
import { downcastAttributeToElement } from '#ckeditor/ckeditor5-engine/src/conversion/downcast-converters';
import { upcastAttributeToAttribute } from '#ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
class LinkTarget extends Plugin {
init() {
const editor = this.editor;
editor.model.schema.extend( '$text', { allowAttributes: 'linkTarget' } );
editor.conversion.for( 'downcast' ).add( downcastAttributeToElement( {
model: 'linkTarget',
view: ( attributeValue, writer ) => {
return writer.createAttributeElement( 'a', { target: attributeValue }, { priority: 5 } );
},
converterPriority: 'low'
} ) );
editor.conversion.for( 'upcast' ).add( upcastAttributeToAttribute( {
view: {
name: 'a',
key: 'target'
},
model: 'linkTarget',
converterPriority: 'low'
} ) );
}
}
For such a long tutorial it surely is a small snippet. Hopefully, most of it is self-explanatory.
First, we expand Schema by defining a new attribute linkTarget that is allowed on text.
Then, we define downcast conversion. downcastAttributeToElement is used as we want to create <a target="..."> element that will be merged with the original <a> element. Keep in mind that the <a> element that is created here has the priority defined to 5, just as in the original link plugin.
The last step is upcast conversion. upcastAttributeToAttribute helper is used, as discussed earlier. In view configuration, it is specified that only target attribute of <a> element should be converted (name: 'a'). This does not mean that <a> element will be converted! This is only filtering configuration for the converter, so it won't convert target attribute of some other element.
Lastly, both converters are added with priority lower than the original converters to prevent any hypothetical problems.
The above sample works for me on the current master of ckeditor5-engine and ckeditor5-link.
Szymon Cofalik's answer is great, but not working anymore for at least the current CKE5 version (34.2.0).
The functions downcastAttributeToElement() and upcastAttributeToAttribute() are not exported anymore, so you have to use attributeToElement() and attributeToAttribute() from the Conversion API, which are available by default.
Updated code example:
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
class LinkTarget extends Plugin {
init() {
const editor = this.editor;
editor.model.schema.extend( '$text', { allowAttributes: 'linkTarget' } );
editor.conversion.for( 'downcast' ).attributeToElement( {
model: 'linkTarget',
view: ( attributeValue, writer ) => {
return writer.createAttributeElement( 'a', { target: attributeValue }, { priority: 5 } );
},
converterPriority: 'low'
} );
editor.conversion.for( 'upcast' ).attributeToAttribute( {
view: {
name: 'a',
key: 'target'
},
model: 'linkTarget',
converterPriority: 'low'
} );
}
}
as I came to the same problem in 2022, I founded this Answer very helpful, I wanted to add id attribute but didn't create my own plugin, I just edited the Link plugin in ckeditor5-build-classic package then I re-builded it.
1- in #module link/linkediting:
Allow link attribute on all inline nodes.
editor.model.schema.extend( '$text', { allowAttributes: ['linkHref', 'linkId'] } );
add conversion for upcast to conserve existed id attribute or create new one:
editor.conversion.for( 'upcast' ).attributeToAttribute( {
view: {
name: 'a'
},
model: {
key: 'linkId',
value: viewElement => {
let id = viewElement.getAttribute( 'id' );
if (id)
return id;
return 'id_'+Math.floor(Math.random() * 10000)
}
},
converterPriority: 'low'
} ) ;
add conversion for editingDowncast, to transform the Model into the view:
editor.conversion.for( 'editingDowncast' ).attributeToElement( {
model: 'linkId',
view: ( attributeValue, conversionApi ) => {
return conversionApi.writer.createAttributeElement( 'a', { id: attributeValue }, { priority: 5 } );
},
converterPriority: 'low'
} ) ;
add conversion for dataDowncast, to get the Attribute when getDate() is called:
editor.conversion.for( 'dataDowncast' )
.attributeToElement( {
model: 'linkId',
view: ( attributeValue, conversionApi ) => {
return conversionApi.writer.createAttributeElement( 'a', { id: attributeValue }, { priority: 5 } );
}
} ) ;
2- in #module link/linkcommand : to create the id, I just wanted a random string so I didn't made any new field in the form, and just added this line after the one that is responsible for linkHref Attribute
writer.setAttribute( 'linkId', 'id_'+Math.floor(Math.random() * 10000), range );
Say you have a list of People incoming from your API.
[{content: 'John'},
{content: 'Tim'},
{content: 'Harry J. Epstein'}]
And you're looking to put people who are first-name-basis friends (John and Tim) under a section 'Friends' and people who are not (Harry J. Epstein) under 'Contacts'.
Tapping a friend selects them with a blue highlight, but tapping a 'contact' selects them with a red highlight.
Would the proper approach be to take the incoming data from the API, add a type: 'Friend', ... or type: 'Contact', ... around it, and section based on that type with separate a FriendItem and ContactItem class so I can split the highlighting function?
I've got a bunch of just basic ListView code that does this exact approach, but I'm basically looking for the easy way out, like Angulars ng-repeat equivalent.
So what's the React Native version of
var friends = api.getFriends()
var contacts = api.getContacts()
<div ng-repeat="friend in friends" ng-click="highlightFriend()"> ... </div>
<div ng-repeat="contact in contacts" ng-click="highlightContact()"> ... </div>
I'm struggling to understand how to split it. Do I need a FriendsPage, FriendsItem, and ContactsItem? Or put everything into one array in FriendsPage and use a FriendsItem that checks if it's a friend or contact and adds a function separately?
I feel like I'm slightly lost coming from MVC. I've got Redux running too, if there's an easy way using that.
Here is a nice example on how you can create section-dependent rows: https://github.com/spoeck/ListViewExample
The idea is basically to create the data blob properly, which is a bit tricky, and then in your renderRow callback, check the sectionID parameter:
_renderRow(rowData: any, sectionID: any, rowID: number) {
if (sectionID === this.data[0].section) {
return <MyFriends />
} else if (sectionID === this.data[1].section) {
return <MyContacts />
}else{
// ...
}
}
why don't you try SectionList
Use the new FlatList or SectionList component instead. Besides
simplifying the API, the new list components also have significant
performance enhancements, the main one being nearly constant memory
usage for any number of rows.
I'm creating dijit widget from input:
<input name="serviceName" type="text"/>
input = new ValidationTextBox({required: true}, query('[name=serviceName]')[0])
I have a lot of inputs and I want to batch-process them, giving them to separate function.
The problem is, that I can't find in docs, how to get the input name back from widget (which can be also Select, DateBox etc., neither I can find that property inspecting element in Firebug
function processInput(input) {
var inputName = ???
}
I've tried input.name and input.get('name'), but they are returning undefined.
When instantiating your widget programmatically, properties are usually not copied. It's only when you use declarative markup that these properties are copied. This means that the name property is not copied from your original input node, so it's in fact empty.
When creating your ValidationTextBox, just provide the name property like the example below:
var inputNode = query('[name=serviceName]')[0];
var input = new ValidationTextBox({
required: true,
name: inputNode.name
}, inputNode);
You can then retrieve the name with either input.name or input.get('name').
I also made an example JSFiddle.