Vue.js - recursive call of the component through render function. (runtime only) - javascript

I have one big request for you. I am a high-school student and I want to create an app for students with my friend. In the begining we wanted to use React for our reactive components, but then we saw Vue and it looked really good. But because of the fact, that we already have a big part of the app written in twig, we didn't want to use Vue.js standalone, because we would have to change a lot of our code, especially my friend, which is writing backend in Sympfony. So we use the runtime only version, which does not have a template option, so i have to write render functions for our components. And i am stucked with one particular problem.
I am writing a file-manager, and i need to render layer for every folder. Code is better then million words, so, take a look please :
var data = {
name: 'My Tree',
children: [
{
name: 'hello',
isFolder: false,
},
{
name: 'works',
isFolder: true,
children: [
{
name: 'child2',
isFolder: true,
},
{
name: 'child3',
isFolder: false,
},
]
}
]
}
Vue.component('layer', {
render: function renderChild (createElement) {
if(data.children.length){
return createElement('ul', data.children.map(function(child){
return createElement('li', {
'class' : {
isFolder: child.isFolder,
isFile: !child.isFolder
},
attrs: {
id: "baa"
},
domProps: {
innerHTML: child.name,
},
on:{
click: function(){
console.log("yes");
},
dblclick: function(){
console.log("doubleclicked");
if(child.children.length){
// if this has children array, create whole "layer" component again.
}
}
}}
)
}))
}
},
props: {
level: {
type: Number,
required: true
},
name: {
type: String,
}
}
})
new Vue({
el: '#fileManagerContainer',
data: data,
render (h) {
return (
<layer level={1} name={"pseudo"}>
</layer>
)
}
})
My question is, how to write that recursive call, which will render the whole Layer component on the doubleclick event, if the element has children array.
Thank you in advance for any reactions, suggestions or answers :)

I know this is a very old question, so my answer won't be useful to the OP, but I wanted to drop in to answer because I found myself with the very same problem yesterday.
The answer to writing these recursive render functions is to not try to recurse the render function itself at all.
For my example, I had a set of structured text (ish) - An array of objects which represent content - which can be nested, like so:
[
// each array item (object) maps to an html tag
{
tag: 'h3',
classes: 'w-full md:w-4/5 lg:w-full xl:w-3/4 mx-auto font-black text-2xl lg:text-3xl',
content: 'This is a paragraph of text'
},
{
tag: 'img',
classes: 'w-2/3 md:w-1/2 xl:w-2/5 block mx-auto mt-8',
attrs: {
src: `${process.env.GLOBAL_CSS_URI}imgsrc.svg`,
alt: 'image'
}
},
{
tag: 'p',
classes: 'mt-8 text-xl w-4/5 mx-auto',
content: [
{
tag: 'strong',
content: 'This is a nested <strong> tag'
},
{
content: ' '
},
{
tag: 'a',
classes: 'underline ml-2',
content: 'This is a link within a <p> tag',
attrs: {
href: '#'
}
},
{
content: '.'
}
]
}
]
Note the nested elements - These would need recursion to render properly.
The solution was to move the actual rendering work out to a method as follows:
export default {
name: 'block',
props: {
block: {
type: Object,
required: true
}
},
methods: {
renderBlock (h, block) {
// handle plain text without a tag
if (!block.tag) return this._v(block.content || '')
if (Array.isArray(block.content)) {
return h(block.tag, { class: block.classes }, block.content.map(childBlock => this.renderBlock(h, childBlock)))
}
// return an html tag with classes attached and content inside
return h(block.tag, { class: block.classes, attrs: block.attrs, on: block.on }, block.content)
}
},
render: function(h) {
return this.renderBlock(h, this.block)
}
}
So the render function calls the renderBlock method, and that renderBlock method is what calls itself over and over if it needs to - The recursion happens within the method call. You'll see that the method has a check to see whether the content property is of an Array type - At this point it performs the same render task, but rather than passing the content as-is, it passes it as an Array map, calling the same render method for each item in the array.
This means that, no matter how deeply the content is nested, it will keep calling itself until it has reached all the way to the "bottom" of the stack.
I hope this helps save someone some time in the future - I certainly wish I'd had an example like this yesterday - I would have saved myself a solid couple of hours!

Related

How to i correctly attatch an instance of a registered component to a scene in Aframe?

I want to create a new Component in Aframe and attach it to my scene. But i don't quite get how it is supposed to be used.
For example i have this component line:
How do i create an instance of the component and attatch it to my scene?
Also how do i correctly set the parameters upon creation?
I have taken a look at the Aframe docs which sais to do as follows:
AFRAME.registerComponent('hello-world', {
init: function () {
console.log('Hello, World!');
}
});
document.querySelector('a-scene').setAttribute('hello-world', '');
but that Example isn't enough for me to know how to use it.
I want to create multiple instances of this component:
I hope you can help me.
AFRAME.registerComponent('line', {
schema: {
color: {type: 'string', default: '#8b4513'},
points: {type: 'array', default: []},
},
init: function () {
console.log(this.data.color);
}
});
Attaching a component called hello-world can be achieved in multiple ways:
// no properties / default
setAttribute('hello-world', '')
// assuming there are properties in the schema
setAttribute('hello-world', 'property', 'value')
setAttribute('hello-world', {
'propertyOne': 'valueOne',
'propertyTwo': 'valueTwo'
})
// another instance - if multiple enabled
setAttribute('hello-world__one', 'property', 'value')
You can have multiple instances of your component attached, if you define its multiple property:
AFRAME.registerComponent('foo',
multiple: true
// .....
})
You can attach multiple components like this:
<a-entity foo="" foo__one="" foo__two=""></a-entity>
fiddle here. It's also quite well documented here.
Taking lines as an example, you can simply have a 'manager' entity, with the lines attached.
<script>
AFRAME.registerComponent('line-manager', {
init: function() {
this.el.setAttribute("line__one", {
"start": '0 0 0",
"end": '1 2 3'
})
this.el.setAttribute("line__two", {
"start": '0 0 0",
"end": '3 2 1'
})
}
})
</script>
<a-scene>
<a-entity line-manager></e-entity>
Maybe this fiddle could help.

Binding ResourceBundle property to list item

In SAPUI5, you can often bind resource bundle properties to items in several ways. I'm attempting to do it in JavaScript, using data provided by an Odata service, but the methods I've found here so far haven't worked. I've tried two different ways:
How to Use Internalization i18n in a Controller in SAPUI5?
binding issue with sap.m.List and model configured in manifest.json
And neither of these have worked. I feel this is because I'm inside a items list, inside of a dialog that's causing my issue:
My code is:
var resourceBundle = this.getView().getModel("i18n");
if (!this.resizableDialog) {
this.resizableDialog = new Dialog({
title: 'Title',
contentWidth: "550px",
contentHeight: "300px",
resizable: true,
content: new List({
items: {
path: path,
template: new StandardListItem({
title: resourceBundle.getProperty("{Label}"),//"{ path : 'Label', fomatter : '.getI18nValue' }",
description: "{Value}"
})
}
}),
beginButton: new Button({
text: 'Close',
press: function () {
this.resizableDialog.close();
}.bind(this)
})
});
getI18nValue : function(sKey) {
return this.getView().getModel("i18n").getProperty(sKey);
},
Using the 2nd method above, never calls the JavaScript method. I didn't think it would work, as it's more JSON based. The first method, loads the data fine, but instead of showing the resource bundle text, it shows just the text found inside of the {Label} value.
The value found inside of {Label} matches the key found inside of the resouce bundle i.e. without the i18n> in front, like you would have in a view.
Anyone have any suggestions?
Use of formatter will solve your problem, but the way you're doing it is wrong. Try this, it will solve your problem.
var resourceBundle = this.getView().getModel("i18n");
if (!this.resizableDialog) {
this.resizableDialog = new Dialog({
title: 'Title',
contentWidth: "550px",
contentHeight: "300px",
resizable: true,
content: new List({
items: {
path: path,
template: new StandardListItem({
title: {
parts: [{ path: "Label" }],
formatter: this.getI18nValue.bind(this)
},
description: "{Value}"
})
}
}),
beginButton: new Button({
text: 'Close',
press: function () {
this.resizableDialog.close();
}.bind(this)
})
});
}
And the formatter function getI18nValue will be like this,
getI18nValue: function(sKey) {
return this.getView().getModel("i18n").getResourceBundle().getText(sKey);
},

Angular with Kendo, Using Grid Values Asynchronously

Ok I'm pretty sure I know exactly what I need to do here but I'm not sure how to do it. Basically I have a grid that I want to make a key column bind to an array of key/values, which I've done before with kendo (not using Angular) and I know that when I'm creating my key/value array asynchronously then that needs to complete before I can get them show-up with kendo, which I have done using promises before.
So here I have the same issue only angular is also involved. I need to fetch and format an array of data into the format in which a kendo grid column can digest it, so no problem here is my controller code:
var realm = kendo.data.Model.define({
id: 'realmID',
fields: {
realmID: { editable: false, nullable: true }
realmType: { type: 'string', validation: { required: true } }
}
})
var ds1 = kendoHelpers.dataSourceFactory('realms', realm, 'realmID')
var realmType = kendo.data.Model.define({
id: 'realmTypeID',
fields: {
realmTypeID: { editable: false, nullable: true },
name: { type: 'string', validation: { required: true } }
}
})
var ds2 = kendoHelpers.dataSourceFactory('realms/types', realmType, 'realmTypeID')
$scope.mainGridOptions = {
dataSource: ds1,
editable: true,
navigatable: true,
autoBind:false,
toolbar: [
{ name: "create" },
{ name: 'save' },
{ name: 'cancel' }
],
columns: [
{ field: 'realmID', title: 'ID' }
{ field: 'realmTypeID', title: 'Realm Type', editor: realmTypesDDL, values: $scope.realmTypeValues },
{ command: "destroy" }
]
}
$scope.secondGridOptions = {
dataSource: ds2,
editable: true,
navigatable: true,
toolbar: [
{ name: "create" },
{ name: 'save' },
{ name: 'cancel' }
],
columns: [
{ field: 'realmTypeID', title: 'ID' },
{ field: 'name', title: 'Name' }
{ command: "destroy" }
]
}
ds2.fetch(function () {
$scope.realmTypeValues = [{ text: 'Test', value: "24bc2e62-f761-4e70-804c-bc36fdeced3d" }];
//this.data().map(function (v, i) {
// $scope.realmTypeValues.push({ text: v.name, value: v.realmTypeID})
//});
//$scope.mainGridOptions.ds1.read()
});
function realmTypesDDL(container, options) {
$('<input />')
.appendTo(container)
.kendoDropDownList({
dataSource: ds2,
dataTextField: 'name',
dataValueField: 'realmTypeID'
});
}
I made this dataSourceFatory helper method above to return me a basic CRUD kendo dataSource that uses transport and also injects an authorization header which is working fine so don't get hung up on that, ultimately I'm going to be using this data in another grid as well as for reference values for the main grid, but I've hard coded some values that I can use to test with in the ds2.fetch callback.
My HTML is pretty plain:
<div>
<h2>Realms</h2>
<kendo-grid options="mainGridOptions"></kendo-grid>
<h2>Realm Types</h2>
<kendo-grid options="secondGridOptions"></kendo-grid>
</div>
This all works fine and well except I am only seeing the GUID of the realmTypeID in the grid, I click it and the editor is populated correctly so that's good but I want the text value to be displayed instead of the GUID. I'm sure the issue is that the array of values is empty whenever angular is binding to the grid options. My questions are:
How do I either delay this bind operation or manually rebind it after the fetch call?
Is there a better way to handle a situation like this? I try not to expend finite resources for no reason (IE making server calls when unnecessary)
Note: When I move the creation of the text/value array to happen before the grid options, I get the desired behavior I am after
EDIT A work around is to not use the directive to create the grid and instead defer the grid creation until the callback of whatever data your column is dependent on, I was hoping for a more elegant solution but this is better than nothing. So your HTML becomes something like
<h2>Realms</h2>
<div id="realms"></div>
<h2>Realm Types</h2>
<kendo-grid options="secondGridOptions"></kendo-grid>
Then you can create the grid in the fetch callback for example:
ds2.fetch(function () {this.data().map(function (v, i) {
$scope.realmTypeValues.push({ text: v.name, value: v.realmTypeID})
});
$('#realms').kendoGrid($scope.mainGridOptions);
$scope.mainGridOptions.dataSource.fetch()
});
But this doesn't feel very angularish so I'm really hoping for a better solution!
Ok...well I think I hacked this enough and without another suggestion I'm going to go forward with this approach. I'm just going to move the binding logic to the requestEnd event of the second grid so that the values array can be populated right before the binding even. I'm also reworking the values array in this method. It is a bit weird though, I think there is some kendo black magic going on with this array because I can't just set it to a new empty array without it breaking completely...which is why I'm poping everything out prior to repopulating the array. That way when something is deleted or edited in the second grid, the DDL in the first grid is updated in the callback.
function requestEnd(e) {
for (var i = $scope.realmTypeValues.length; i >= 0; i--) $scope.realmTypeValues.pop();
var data;
if (e.type == "read")
data = e.response;
else
data = e.sender.data();
data.map(function (v, i) { $scope.realmTypeValues.push({ text: v.name, value: v.realmTypeID }); });
if ($('#realms').data('kendoGrid') == undefined) {
$('#realms').kendoGrid($scope.mainGridOptions);
}
else
$('#realms').data('kendoGrid').columns[4].values = $scope.realmTypeValues;
}
ds2.bind('requestEnd', requestEnd);
So I'm going to accept my own answer unless anyone has a better approach!

Make ember to resolve hasMany relationship when loading

I'm currently facing a big problems for days. I'm using ember simple-auth plugin which provide me a session object accessible through the code or the templates. That session object store the account information such as username, id and rights.
My models are like this :
App.Right = DS.Model.extend({
label: DS.attr('string', { defaultValue: undefined })
});
App.Right.FIXTURES = [
{
id: 1,
label: 'Admin'
}, {
id: 2,
label: 'Manager'
}, {
id: 3,
label: 'User'
}
];
App.User = DS.Model.extend({
username: DS.attr('string'),
rights: DS.hasMany('right', {async: true})
});
App.User.FIXTURES = [
{
id: 1,
username: "Someone",
rights: [1]
}
];
Then I have (as specified on the simple-auth documentation) this setup :
App.initializer({
name: 'authentication',
initialize: function(container, application) {
Ember.SimpleAuth.Session.reopen({
account: function() {
var userId = this.get('userId');
if (!Ember.isEmpty(userId)) {
return container.lookup('store:main').find('user', userId);
}
}.property('userId')
});
...
}
});
Inside one of my view I'm doing this:
this.get('context.session.account.rights').toArray()
but it gives me an empty array. That piece of code is executed inside an Ember.computed property.
The question is how can I resolve the childrens of account before rendering the view ?
Since async: true this.get('context.session.account.rights') will return a promise object so you will have to use this.get('context.session.account.rights').then(... see: http://emberjs.com/api/classes/Ember.RSVP.Promise.html#method_then
Okay so I finally got it to work. It doesn't solve the original question because the original question was completely stupid. It's just IMPOSSIBLE to resolve relationships synchronously when you use the async: true. Trying to resolve it in advance is NOT the solution because you will still not know when it has actually resolved.
So here is the solution:
$.each(this.get('cellContent.buttonList'), function(i, button) {
button.set('hasAccess', false);
this.get('context.session.account').then(function(res) {
res.get('rights').then(function(result) {
button.set('hasAccess', Utils.hasAccess(result.toArray(), button.rights));
});
});
});
Using the following cellContent.buttonList definition:
buttonList: [
Ember.Object.create({
route: 'order',
label: 'Consult',
rights: 'all'
}), Ember.Object.create({
route: 'order.edit',
label: 'Edit',
rights: [1, 2]
})
]
Explanation
We have to use Ember.Object in order to have access to the set method. Using an Ember object is very handy. It allows us to change the value of properties after the render process making the view to update according to the new value you just set.
Because it updates the view, you don't have to care anymore whether your model has resolved or not.
I hope this will help people as much as it helps me.

How to display nested data items in FuelUX tree bootstrap plugin

I'm trying to implement the FuelUX tree plugin and I've followed the example so far but I need a nested structure. I'm assuming the tree plugin is capable of handling nested children? is this correct?
var treeDataSource = new TreeDataSource({
data: [
{ name: 'Test Folder 1', type: 'folder', additionalParameters: { id: 'F1' },
data: [
{ name: 'Test Sub Folder 1', type: 'folder', additionalParameters: { id: 'FF1' } },
{ name: 'Test Sub Folder 2', type: 'folder', additionalParameters: { id: 'FF2' } },
{ name: 'Test Item 2 in Folder 1', type: 'item', additionalParameters: { id: 'FI2' } }
]
},
{ name: 'Test Folder 2', type: 'folder', additionalParameters: { id: 'F2' } },
{ name: 'Test Item 1', type: 'item', additionalParameters: { id: 'I1' } },
{ name: 'Test Item 2', type: 'item', additionalParameters: { id: 'I2' } }
],
delay: 400
});
So far it seems to load the top level items into the opened folders rather than the nested data items. This is what the demo on their site also does but this doesn't seem as if its the desired interaction. Can anyone confirm if this is expected behaviour?
Can anyone point me to code where they've created a nested data tree using this plugin? Is there something really obvious I am missing?
I am actually in the process of writing a blog post on this very issue.
The solution I have developed is not for the faint-of-heart. The problem is that the folder objects do not support instantiation with child data. Also, adding children is no trivial task. I spun up a quick fiddle that you can pick through to get an idea of how to accomplish your goal. I am using this same solution only that my addChildren function calls out to an MVC route via AJAX and gets back a JSON object to populate the children dynamically.
You can literally, copy and paste the code from my fiddle and start using the addChildren function out-of-the-box.
I'm sorry for the lack of documentation about this - it needs to be improved for sure.
The idea is that you provide a dataSource when instantiating the tree control, and that data source should have a data function with the signature (options, callback). That data function will be called on control init to populate the root level data, and will be called again any time a folder is clicked.
The job of the data function is to look at the options parameter which is populated from the jQuery.data() on the clicked folder and respond with the data for that folder. The special case is the initial root folder data, where the options are populated from any jQuery.data() on the control's root div, which may or may not exist.
The jQuery.data() on folders is populated from the array of objects you provide in your data function's callback. You can see in this example https://github.com/ExactTarget/fuelux/blob/master/index.html#L184-L189 there is a property called additionalParameters but really you can provide any additional properties beyond the required name and type for you to use later (the next time your data function is called) to determine which folder was clicked and return the data for that folder.
Our current example returns the same static data for every folder, which is not the best example, so I do hope to improve this situation by either creating a tutorial myself or linking to one if someone beats me to it.
Following up on Adam's answer, here is an example that seems to accomplish what you want..
The data function for the DataSource can check to see if there is "sub" data passed via options:
DataSource.prototype = {
columns: function () {
return this._columns;
},
data: function (options, callback) {
var self = this;
if (options.search) {
callback({ data: self._data, start: start, end: end, count: count, pages: pages, page: page });
} else if (options.data) {
callback({ data: options.data, start: 0, end: 0, count: 0, pages: 0, page: 0 });
} else {
callback({ data: self._data, start: 0, end: 0, count: 0, pages: 0, page: 0 });
}
}
};
Demo on Bootply: http://www.bootply.com/60761

Categories

Resources