This is the reason...
NOW
Image Credit: @phenomlab 😉
unknownjjj.png
Absolutely brilliant @baris, the difference is clear 👍
PREVIOUSLY
Screenshot_2021-05-10 TalkCounty com(3).png
I'm attempting to write my very first plugin which should allow me to add custom attributes / data fields to a category. This information should be updated in the Redis datastore after the form has been submitted.
After reading the documentation, scanning the forums, and browsing the code of existing plugins for a few days, I'm still a little unsure of how exactly I should develop a plugin to achieve this functionality.
First of all, I want to ask if this is even possible? If it is possible, I'm wondering if you lovely people could guide me towards the right direction to implementing this?
I've broken down my goals into sub-goals like so:
When the 'add category' view loads
When the 'add category' view is submitted
When the 'edit category' view is loaded
I know that my plugin will require the use of injecting some html into a view with the help of jQuery, then posting custom data and inserting it into Redis. Apart from that, my first problem is that I'm finding it hard to understand which hook to use from the list of hooks as there is no description along with them. I'm taking a guess and saying action:category.create
but I can't figure out where to go from here as the examples in the docs are just so simple.
Any code samples, pointers, or any general help would be fantastic. I'm just a little confused by the very brief documentation atm but I really want to get good at this stuff!
Thanks
The hooks that you want to use are filter:category.create
on category creationg and filter:category.update
when a category is updated.
filter:category.create
is passed the category object and the data that is being used to create the category, I just added that now.
filter:category:updated
is passed the fields that are being modified.
@baris Thanks for your reply and for committing the new hook code.
Unfortunately, I can't get any further than the code I have here as I can't figure out how to return categoryObj
back to the parent code in a callback. Time to give up and go back to PHP I think
(function(MyPlugin) {
'use strict'
MyPlugin.myMethod = function(categoryObj) {
// do something with the data
categoryObj.category.homepageSection = 1;
console.log(categoryObj);
},
MyPlugin.myMethod1 = function(categoryObj) {
// do something with the data
console.log(categoryObj);
}
}(module.exports))
Don't give up so easily
All the filters take a callback parameter which you can call after you are done modifying the data.
In your case it would look something like this
MyPlugin.myMethod = function(categoryObj, callback) {
// do something with the data
categoryObj.category.homepageSection = 1;
console.log(categoryObj);
callback(null, categoryObj);
};
The first param is an error object and then you pass the data that you modified back into the callback so other plugins that listen for the same hook can keep on modifying the data.
baris said:
Don't give up so easily
Yes! Yes! Yes!
JavaScript is a prototypal language, which makes it a bit confusing, but very powerful. You can structure your programs in various ways. I'm only gonna go into how it's (mostly) done in nodeJS:
Although you define objects with members, it is uncommon to actually create instances of those objects. JS has a new
keyword that allows you to do that, but there it gets messy with this
and what it means. Sometimes this
points to an object, sometimes a function - it all depends on where you use it and who called what. You should read up on 'function scope' and 'closure', in case you're interested. I myself still like to use it. But anyway, most code you will encounter treats objects as static
. Also, function parameters are passed by value
rather than by reference
(you may know this from C as passing a variable vs. passing a pointer). Let's have a look:
var aNumberVariable = 50;
myMethod = function(functionsNumberVariable) {
functionsNumberVariable = 100;
console.log(functionsNumberVariable);
};
myMethod(aNumberVariable);
console.log(aNumberVariable);
When you execute this snippet, the output will read 100
and then 50
.
Here you can see, that aNumberVariable
is assigned 50
, myMethod
defined and then called. The parameter is passed to the function, where only its value is received, and put into a newly created variable functionsNumberVariable
. This then is set to 100 and written to the console. The function call (myMethod()
) is done synchronously, so everything following 'waits' for the function to finish/return. It does so with no value (aka undefined
), but the console ignores that. Then the last line is executed, showing that aNumberVariable
's value is still 50. (The showing undefined
in the picture is actually not from anywhere in my code. I think that is the result of a function, the console's VM wraps your statements in. Once again the scope thingy.)
But this also means, that once myMethod
is through doing its thing, you have no way to access functionsNumberVariable
anymore (before mentioned closure or function scope). Even worse, it's a candidate to be gobbled up by the garbage collector in one of its next runs. Now, you could, of course, easily just return its value, but this is where nodeJS's magic of asynchronicity gets in between.
Everything below would have to wait for your function to return, so a read from a database or from a network resource could easily block your process for a lengthy amount of time - not so good.
Fortunately, JavaScript is capable of passing functions as parameters. . This - we're almost there - is the callback (in our case here; you can do heaps of other neat things with it). You basically pass the next step in your chain of processing as this function, passing it the values which you otherwise would have returned. I fail to come up with a quick and easy code example to run, right now. So I'll draw it in a diagram:
At this point you'd be perfectly right to say: "Well, nice story. But there's also an invisible return in queryDatabase
which the program has to wait for.". Yes, it is. That's why I painted its declaration yellowish. Your JavaScript runs also in nodeJS on a single thread, but under the hood, when V8 (JS engine) passes such calls to the I/O layer it utilises multi-threading. This game of passing callbacks is played along the chain, until V8 'queries' some system I/O function. There it is split off into another thread, from where (on the return path, if you will) the callbacks are called one after another. The actual functions return
s are done as soon V8 has branched off this new thread. I'm not 100% sure about this last part, though. I urge everyone who knows more about this to correct me!
By the way: Every function comes with several 'invisible' variables set. One of which is arguments
. That's an array of all parameters passed to the function. Disregarding the declarations (function(var1, var2)) you did. So even when you define your function as myMethod = function()
, arguments
is available and filled with everything that has been passed in the call. Great for exploring!
I do hope @Joel-Murphy comes back and keeps at it.
Ah thank you, I was so close
Wow thank you so much for your long, detailed post. I decided I needed a refresher on JS after reading your post, so that's where I've been for the past few days. I completely forgot about closures, but now I think I'm back up to scratch with them so things should make a lot more sense now
@BDHarrington7
Oh I will don't worry! I've already started translating NodeBB to Welsh and am keen to get good at plugin development.
One thing I'm still unsure about though is how logic works in templates. Without logic inside of statements, doesn't it make it near impossible to do anything more than show or hide certain elements?
Assume I want to order (or more precisely, group) categories on the homepage, how should I do this?
<ul class="categories" itemscope itemtype="http://www.schema.org/ItemList">
<p>A few threads:</p>
<!-- BEGIN categories -->
{categories.homepage_section}
put categories with a homepage_section value of '1' here.
<!-- IMPORT partials/categories/item.tpl -->
<!-- END categories -->
<li class="line-seperator"></li>
<p>Other threads:</p>
<!-- BEGIN categories -->
{categories.homepage_section}
put categories with a homepage_section value of '2' here.
<!-- IMPORT partials/categories/item.tpl -->
<!-- END categories -->
</ul>
This should produce something like this:
A few threads
Other threads
Anyone have any ideas? The only obvious solution I can think of right now is adding attributes such as homepage_section1 = true
and homepage_section2 = false
to each category. But this seems very long winded and hardly efficient.
Thanks
Okay, after more fiddling around, I've figured out even more about writing plugins. I've now written some code using client side javascript to group each category together according to its homepageSection
id. Now I just need to figure out how to render the page after asynchronously rendering each category item.
$(document).ready(function() {
console.log('Client side JS loaded');
$(window).on('action:ajaxify.contentLoaded', function(ev, data) {
if (data.tpl === 'categories') {
var sections = {};
var sectionNames = ['A few threads', 'Other threads'];
var categoriesHTML = "";
// Loop over each category and assign them to the section array based on their homepageSection value.
for(var i = 0; i < ajaxify.data.categories.length; i++){
if( (typeof ajaxify.data.categories[i].homepageSection != 'undefined') && (ajaxify.data.categories[i].hasOwnProperty("homepageSection") )){
sections["section"+ajaxify.data.categories[i].homepageSection] = sections["section"+ajaxify.data.categories[i].homepageSection] || [];
sections["section"+ajaxify.data.categories[i].homepageSection].push(ajaxify.data.categories[i]);
}
}
// Loop over each section in the sections array, then render each category in each section
for(section in sections){
for (var i = 0; i < sections[section].length; i++) {
var myData = sections[section][i];
ajaxify.loadTemplate('partials/categories/item', function(myTemplate) {
var html = templates.parse(myTemplate, myData);
categoriesHTML += html;
;
// loadTemplate is asynchronous and so this is where I suck @ node. Time for some more learning...
});
};
}
}
});
});
Slow progress, but I'm nearly there
Get the template first, then parse.
$(document).ready(function() {
console.log('Client side JS loaded');
$(window).on('action:ajaxify.contentLoaded', function(ev, data) {
if (data.tpl === 'categories') {
var sections = {};
var sectionNames = ['A few threads', 'Other threads'];
var categoriesHTML = "";
// Loop over each category and assign them to the section array based on their homepageSection value.
for(var i = 0; i < ajaxify.data.categories.length; i++){
if( (typeof ajaxify.data.categories[i].homepageSection != 'undefined') && (ajaxify.data.categories[i].hasOwnProperty("homepageSection") )){
sections["section"+ajaxify.data.categories[i].homepageSection] = sections["section"+ajaxify.data.categories[i].homepageSection] || [];
sections["section"+ajaxify.data.categories[i].homepageSection].push(ajaxify.data.categories[i]);
}
}
ajaxify.loadTemplate('partials/categories/item', function(myTemplate) {
// Loop over each section in the sections array, then render each category in each section
for(section in sections){
for (var i = 0; i < sections[section].length; i++) {
var myData = sections[section][i];
var html = templates.parse(myTemplate, myData);
categoriesHTML += html;
};
}
// Done parsing, add it.
$('.categories').html(categoriesHTML);
});
}
});
});
Oh you're right! Not sure why I was loading the template multiple times! Thanks
This is the result I get. I'm guessing some further rendering functions need to be called after the template is appended to the DOM?
Another update:
The rendering is actually working fine.
The following HTML:
<small>{../description}</small>
Should be changed to:
<small>{description}</small>
To make those properties show.
However, functions within the theme don't appear to be working. E.g.
<div class="icon hidden-sm hidden-xs pull-left" style="{function.generateCategoryBackground}">
<i class="fa fa-fw {../icon}"></i>
</div>
I'm guessing this is due to the way I'm passing an object to a template when it's being rendered?
Edit:
I've tried defining a test helper inside of main.js
:
templates.registerHelper('testingz',function(data, iterator, numblocks) {
console.log('{function.testingz}');
return "testingz";
});
And calling it from my HTML template using {function.testingz}
but nothing happens. Am I doing this correctly? As soon I figure this out, I should be able to finish my first plugin
Hmm, not sure.Templates is @psychobunny 's thing. Maybe he has the treasure.