Access template options in module's construct method - javascript

In Apostrophe, I have a custom module where I would like to pass an option from the Nunjucks apos.area call to the construct method of the widget itself. Concretely, I want to adjust the output of getWidgetWrapperClasses based on the options passed to the module in the template. Is this possible?
Here's an example of what I would like to achieve:
lib/modules/example-widgets/index.js
module.exports = {
extend: "apostrophe-widgets",
label: "Example widget",
construct: function(self, options) {
self.getWidgetWrapperClasses = function(widget) {
// templateOptions would be the options object as defined
// in home.html below
return ["column", "column-" + templateOptions.width];
};
}
};
lib/modules/apostrophe-pages/views/pages/home.html
{% extends "layout.html" %}
{% block content %}
<div id="widgets">
{{ apos.area(data.page, "example", {
widgets: {
"example": {
width: "half"
}
}
}) }}
</div>
{% endblock %}

I solved this by not using the getWidgetWrapperClasses method, but instead extending the widget wrapper template and overriding a Nunjucks block in there. This is in fact a documented approach if you look in lib/modules/apostrophe-areas/views/widgetBase.html in Apostrophe's code.
I changed lib/modules/example-widgets/index.js like this:
module.exports = {
extend: "apostrophe-widgets",
label: "Example widget",
wrapperTemplate: "wrapper",
construct: function(self, options) {
// Do something
}
};
Then, I added a lib/modules/example-widgets/views/wrapper.html file. In that file, you can simply override the extraWrapperClasses block to add the classes you want, all the while having access to the template options through data.options.
{% extends "apostrophe-areas:widget.html" %}
{% block extraWrapperClasses %}column column-{{ data.options.width }}{% endblock %}

Related

Add a download functionality in django change view

I already added a button alongside the save buttons for my django change view:
{% extends 'admin/change_form.html' %}
{% load static %}
{% load i18n %}
{% block submit_buttons_bottom %}
<div class="submit-row">
<input type="submit" value="Download" id="download_profile" name="_continue">
<input type="submit" value="{% trans 'Save' %}" class="default" name="_save">
<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother">
<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue">
<!-- inmport script -->
<script src="{% static '/js/downloadData.js' %}"></script>
<script src="{% static '/js/collapsibleSection.js' %}"></script>
<script src="{% static '/js/investmentAdminScript.js' %}"></script>
{% endblock %}
and I already started adding the script to retrieve individual data per row to download them.
'use strict';
window.addEventListener('load', () => {
const downloadButton = document.querySelector('#download_profile');
downloadButton.addEventListener('click', e => {
if (!confirm('Data will be saved before downloading. Are you sure you want to continue?')) {
e.preventDefault();
return;
}
// get module
const fieldsets = document.querySelectorAll('fieldset');
fieldsets.forEach(thisFieldset => {
const dataRows = thisFieldset.querySelectorAll('.form-row');
dataRows.forEach(dataRow => {
// retrieve each input field and add a download to .xlsx function
});
});
});
});
However, it gets complicated very fast. Is there a better way to do this?
Found a single JS file that allows you to format .xlsx files and start a download with one function
https://github.com/egeriis/zipcelx
Since you're using django, download the "standalone.js" file and add it to your static files. Reference it in your template in a script tag. Now you'll have access to the "zipcelx()" function
Here's a "getting started" example I made
const config = {
filename: "practice-file",
sheet: {
data: [
// Row 1
[
{value: "row1 value1",type:"string"},
{value: "1000",type: 'number'}
],
// Row 2
[
{value: "row2 value1", type: 'string'},
{value: "row2 value2", type: "string"}
]
]
}
}
// call zipcelx to immediately start the download in client's browser
zipcelx(config)
Example of the File Generated
Don't know how large your data sets are, might get tedious with having to format your data in such a way. Some helper functions should do the trick if that became a problem.
Hope this helped!

How to attach a callback on a recursively generated <select> dropdown?

I'm trying to implement chained dependent dropdown combobox selection, so you start with one combobox for main category and once you select main category, another <select> appears to select a subcategory, and so on until the innermost (most specific) category is selected. The code I have currently only works for one subcategory (direct children), how can I make it work for other levels too? So, I need to attach an onChange callback to a newly created <select> somehow.
This is jQuery code in my Django template:
{% extends 'pages/base.html' %}
{% block content %}
<h1>Create a product</h1>
<form method='POST' id='productForm' data-products-url="{% url 'products:ajax_load_categories' %}">
{{ form.as_p }}
</form>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
$("select").change(function () {
var url = $("#productForm").attr("data-products-url");
var categoryId = $(this).val();
$.ajax({
url: url,
data: {
'category': categoryId
},
success: function (data) {
$("#productForm").append(data);
}
});
});
</script>
{% endblock %}
Here is my view:
def load_categories(request):
category_id = request.GET.get('category')
subcategories = Category.objects.get(id=category_id).get_children()
return render(request, 'products/category_dropdown_list_options.html', {'subcategories': subcategories})
products/category_dropdown_list_options.html
<select id="select_{{ subcategories.first.get_level }}">
<option value="">---------</option>
{% for subcategory in subcategories %}
<option value="{{ subcategory.pk }}">{{ subcategory.name }}</option>
{% endfor %}
</select>
Here is my urls.py:
app_name = 'products'
urlpatterns = [
path('create/', product_create_view, name='product-create'),
path('ajax/load-categories/', load_categories, name='ajax_load_categories')
]
Here is my Category model as per request:
from mptt.models import MPTTModel, TreeForeignKey
class Category(MPTTModel):
parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
name = models.CharField(max_length=255)
slug = models.SlugField()
class Meta:
unique_together = (('parent', 'slug',))
verbose_name_plural = 'categories'
class MPTTMeta:
order_insertion_by = ['name']
def __str__(self):
return self.name
def save(self, *args, **kwargs):
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def get_slug_list(self):
ancestors = self.get_ancestors(include_self=True)
slugs = [ancestor.slug for ancestor in ancestors]
new_slugs = []
for idx, ancestor in enumerate(slugs, 1):
new_slugs.append('/'.join(slugs[:idx]))
return new_slugs
def get_recursive_product_count(self):
return Product.objects.filter(category__in=self.get_descendants(include_self=True)).count()
You'll need to turn your jQuery ajax script into a function, then call it recursively, like this:
<script>
var $r_ = function() {
var url = $("#productForm").attr("data-products-url");
var categoryId = $(this).val();
$.ajax({
url: url,
data: {
'category': categoryId
},
success: function (data) {
if (data != 'leaf_node') {
$("#productForm").append(data);
}
$('select').change($r_);
}
});
} //end of $r_
$('select').change($r_);
</script>
Update
If you take a look at the get_children method of the MPTT model, you'll see that it checks whether or not the instance has any children, and returns None if it doesn't.
Add a None check in your view, then add a different response when you've reached a leaf node:
from django.http import HttpResponse
def load_categories(request):
category_id = request.GET.get('category')
subcategories = Category.objects.get(id=category_id).get_children()
if subcategories:
return render(request, 'products/category_dropdown_list_options.html', {'subcategories': subcategories})
return HttpResponse('leaf_node')
Then add a check for leaf nodes in your ajax call (see above).

Request to Apostrophe’s server

I’d like to make a request in my j=Javascript code to the Apostrophe’s server (like PUT, POST…). I did that but it does not work:
{% extends "layout.html" %}
{% block main %}
<div class="main-content">
<h3>Hello world!
{% if not data.user %}
<a class="login-link" href="/login">Login</a>
{% endif %}
</h3>
<p>This is a very bare bones Apostrophe project. Now, get to work and make a real website!</p>
</div>
{{
apos.area(data.page, 'body', {
widgets: {
'apostrophe-images': {
size: 'full'
},
'apostrophe-rich-text': {
toolbar: [ 'Styles', 'Bold', 'Italic', 'Link', 'Unlink' ],
styles: [
{ name: 'Heading', element: 'h3' },
{ name: 'Subheading', element: 'h4' },
{ name: 'Paragraph', element: 'p' }
]
}
}
})
self.apos.tasks.add(self.__meta.name, 'insert-stuff', function(apos, argv, callback) {
var req = self.apos.tasks.getReq();
return self.find(req, { cool: true }).toArray().then(function(err, pieces) {
if (err) {
return callback(err);
}
});
};
}}
{% endblock %}
When I run the script (going to localhost:3000), I receive an error in my console. I have the error :
e.stack: Template render error: (apostrophe-pages:pages/home.html) [Line 34, Column 3]
expected variable end
at Object.exports.prettifyError (C:\Windows\System32\test-project\node_modules\nunjucks\src\lib.js:34:15)
at new_cls.render (C:\Windows\System32\test-project\node_modules\nunjucks\src\environment.js:469:27)
at Object.self.renderBody (C:\Windows\System32\test-project\node_modules\apostrophe\lib\modules\apostrophe-templates\index.js:309:47)
at Object.self.renderForModule (C:\Windows\System32\test-project\node_modules\apostrophe\lib\modules\apostrophe-templates\index.js:176:19)
at Object.self.render (C:\Windows\System32\test-project\node_modules\apostrophe\lib\modules\apostrophe-module\index.js:173:34)
at Object.self.renderPageForModule (C:\Windows\System32\test-project\node_modules\apostrophe\lib\modules\apostrophe-templates\index.js:666:28)
at C:\Windows\System32\test-project\node_modules\apostrophe\lib\modules\apostrophe-module\index.js:349:31
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:726:13
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:52:16
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:269:32
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:44:16
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:723:17
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:167:37
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:52:16
at iterate (C:\Windows\System32\test-project\node_modules\async\lib\async.js:260:24)
at Object.async.forEachOfSeries.async.eachOfSeries (C:\Windows\System32\test-project\node_modules\async\lib\async.js:281:9)
:: 2018-08-17T10:43:23+0200: template error at /
Current user: admin
{ Template render error: (apostrophe-pages:pages/home.html) [Line 34, Column 3]
expected variable end
at Object.exports.prettifyError (C:\Windows\System32\test-project\node_modules\nunjucks\src\lib.js:34:15)
at new_cls.render (C:\Windows\System32\test-project\node_modules\nunjucks\src\environment.js:469:27)
at Object.self.renderBody (C:\Windows\System32\test-project\node_modules\apostrophe\lib\modules\apostrophe-templates\index.js:309:47)
at Object.self.renderForModule (C:\Windows\System32\test-project\node_modules\apostrophe\lib\modules\apostrophe-templates\index.js:176:19)
at Object.self.render (C:\Windows\System32\test-project\node_modules\apostrophe\lib\modules\apostrophe-module\index.js:173:34)
at Object.self.renderPageForModule (C:\Windows\System32\test-project\node_modules\apostrophe\lib\modules\apostrophe-templates\index.js:666:28)
at C:\Windows\System32\test-project\node_modules\apostrophe\lib\modules\apostrophe-module\index.js:349:31
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:726:13
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:52:16
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:269:32
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:44:16
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:723:17
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:167:37
at C:\Windows\System32\test-project\node_modules\async\lib\async.js:52:16
at iterate (C:\Windows\System32\test-project\node_modules\async\lib\async.js:260:24)
at Object.async.forEachOfSeries.async.eachOfSeries (C:\Windows\System32\test-project\node_modules\async\lib\async.js:281:9) name: ‘Template render error’ }

How to namespace with twig.js

I can see with twig.js that you can use namespacing for template paths:
https://github.com/justjohn/twig.js/wiki#user-content-namespaces
Where do you you specify the namespaces? Here it is in the documentation:
var template = Twig.twig({
data: your-template,
namespaces: { 'my-project': 'path/to/views/folder/' }
}).render();
That IS the specification...i.e. the next lines of the doc:
Ex:
{# your-template.twig #}
{% extends "my-project::template.twig" %}
The "my-project::" will now point to "path/to/views/folder/".

nunjucks set create object

As nunjucks now supports using set as a block I wanted to do something like this:
{% set navigationItems %}
{% for item in items %}
{ name: item.name, url: item.url }{% if not loop.last %},{% endif %}
{% endif %}
{% endset %}
Then call this variable as the input object on another macro, like so:
{{ navigation(items=[navigationItems]) }}
However, navigationItems is evaluated as a string, not an array-literal. Any idea how, or if this is possible?
Thanks.
I'm not exactly sure what you're trying to accomplish. It looks like you want to loop over one array called items and copy it into a new array called navigationItems. Perhaps items contains more keys than you want to pass to the macro?
I'm going to make that assumption, otherwise you could simply copy items into navigationItems like so:
{% set navigationItems = items %}
This example works:
{% macro navigation(items) %}
<ul>
{% for item in items %}
<li>{{ item.name }} - {{ item.url }}</li>
{% endfor %}
</ul>
{% endmacro %}
{% set websites = [
{
name: 'Google',
url: 'http://google.com',
description: 'A search engine'
},
{
name: 'GitHub',
url: 'http://github.com',
description: 'A webapp for your git repos'
},
{
name: 'StackOverflow',
url: 'http://stackoverflow.com',
description: 'The answer: 42'
}] %}
{% set navigationItems = [] %}
{% for website in websites %}
{% set navigationItems = (navigationItems.push({name: website.name, url: website.url}), navigationItems) %}
{% endfor %}
{{ navigation(items=navigationItems) }}
websites values contain a description key which is not passed on to the navigationItems array. If it were me, I'd just pass website directly to the navigation macro since your keys: name and url are the same in both arrays.
The pattern here is almost like a map method in Javascript or Ruby.

Categories