Extend object on Meteor - meteor

When i add/edit blogPost, i've my object with all properties. My code :
Add post :
Template.postListAdmin.events({
'submit form': (e) => {
// Prevent default browser form submit
e.preventDefault();
let image = $('#js-image-uploaded'),
draft = $('[name="draft"]'),
isSmall = false,
isDrafted = false;
// If post draft, return true
if (draft.is(':checked')) isDrafted = true;
// If post image is small
// return true for add 'small' classe
if (image.height() < 80) isSmall = true;
let post = {
title: $('[name="title"]').val(),
image: image.attr('src'),
isSmall: isSmall,
description: $('[name="description"]').val(),
category: $('[name="category"]').val(),
time: $('[name="time"]').val(),
dateCreated: dateFormat($('[name="dateCreated"]').val(), 'yyyy-mm-dd'),
content: $('[name="content"]').val(),
draft: isDrafted
};
Meteor.call('posts.insert', post);
setTimeout(() => {
$('#js-post-form')
.toggleClass('is-hidden')
.find('input, textarea').val('');
}, 500);
}
});
Edit post :
Template.postEdit.events({
'submit form': function (e) {
e.preventDefault();
let image = $('#js-image-uploaded'),
draft = $('[name="draft"]'),
isSmall = false,
isDrafted;
if (draft.is(':checked')) isDrafted = true;
else isDrafted = false;
if (image.height() < 80) isSmall = true;
let post = {
slug: $('[name="title"]').val(),
title: $('[name="title"]').val(),
image: image.attr('src'),
isSmall: isSmall,
description: $('[name="description"]').val(),
category: $('[name="category"]').val(),
time: $('[name="time"]').val(),
dateCreated: dateFormat($('[name="dateCreated"]').val(), 'yyyy-mm-dd'),
dateModified: new Date(),
content: $('[name="content"]').val(),
draft: isDrafted
};
Meteor.call('posts.edit', this._id, post);
Router.go('postListAdmin');
},
});
I would like optimize my code and avoid creating my object 'post' 2x.
Do you have any idea how i can optim this ?
Thank you every boby :)

You should be able to achieve what you want to do by defining post without the let keyword.
For example:
post = {
title: $('[name="title"]').val(),
image: image.attr('src'),
isSmall: isSmall,
description: $('[name="description"]').val(),
category: $('[name="category"]').val(),
time: $('[name="time"]').val(),
dateCreated: dateFormat($('[name="dateCreated"]').val(), 'yyyy-mm-dd'),
content: $('[name="content"]').val(),
draft: isDrafted
};
You will have to decide how you want to handle the two instances, though, since they are not exactly the same. Also, moving the variable definition outside of either file might be helpful for organizing your code. You could use a directory on the client named utils, and add a file that contains your global variable definitions.

Related

Generate paginated URLs like ?page=2, ?page=3 etc. in NextJS getStaticPaths and access the query params in getStaticProps

I am building a NextJS based site with multiple locales (different domains) where the data comes from storyblok CMS (folder level translation).
I am trying to figure out the best approach to statically generate the paginated URLs for the blog and since the data is known at build time, I figured the best approach would be to generate all URLs in getStaticPaths and then fetch the data for each Page in getStaticProps. This works fine for routes without parameters but when returning a page parameter along with the slug parameter in getStaticPaths, I cannot access it in getStaticProps.
I know that query params cannot be accessed in getStaticPaths because we cannot know the custom querys at buildtime, but in this specific case, we actually can since these paths are generated in getStaticProps.
pages/[[...slug]].jsx
import {
useStoryblokState,
getStoryblokApi,
StoryblokComponent,
} from "#storyblok/react";
export default function Page({
story,
locale,
locales,
defaultLocale,
stories,
}) {
story = useStoryblokState(story, {
// language: locale,
});
return (
<div>
<StoryblokComponent
blok={story.content}
storyData={story}
stories={stories}
/>
</div>
);
}
export async function getStaticProps({
locale,
locales,
defaultLocale,
params,
}) {
console.log(params.slug); // This logs the slug
console.log(params.page); // This logs undefined
console.log(params.query.page); // This logs undefined
// Empty slug on front page
// Make sure root element page pr folder are selected in storyblok
let slug = params.slug ? params.slug.join("/") : "";
let sbParams = {
version: "draft",
resolve_relations: relationsResolvers,
language: locale,
};
let { data } = await getStoryblokApi().get(
`cdn/stories/${locale}/${slug}`,
sbParams
);
let sbIndexParams = {
version: "draft",
resolve_relations: relationsResolvers,
per_page: 10,
page: params.page || 1,
starts_with: `${locale}/${slug}`,
sort_by: "first_published_at:desc",
language: locale,
filter_query: {
component: {
in: "page,post,case,template",
},
},
};
/* fetch an array of stories if page is startpage */
let storiesData = null;
if (data.story.is_startpage) {
storiesData = await getStoryblokApi().get(`cdn/stories`, sbIndexParams);
}
return {
props: {
story: data ? data.story : false,
key: data ? data.story.id : false,
stories:
data.story.is_startpage && storiesData
? storiesData.data.stories
.filter((story) => story.is_startpage == false)
.map((story) => {
return {
name: story.name,
created_at: story.created_at,
published_at: story.published_at,
id: story.id,
uuid: story.uuid,
slug: story.slug,
full_slug: story.full_slug,
is_startpage: story.is_startpage,
content: {
cover: story.content.cover ?? null,
cover_image: story.content.cover_image ?? null,
author: story.content.author ?? null,
category: story.content.category ?? null,
},
};
})
: false,
locale,
locales,
defaultLocale,
},
revalidate: 3600,
};
}
export async function getStaticPaths({ locales }) {
let { data } = await getStoryblokApi().get("cdn/links/", {
is_folder: false,
filter_query: {
component: {
in: "page,post,case,template",
},
},
});
let paths = [];
Object.keys(data.links).forEach((linkKey) => {
if (data.links[linkKey].is_folder) {
return;
}
// get array for slug because of catch all
const slug = data.links[linkKey].slug;
let splittedSlug = slug.split("/");
const linkLocale = splittedSlug[0];
splittedSlug.shift();
if (splittedSlug == "") splittedSlug = false;
// create additional languages
for (const locale of locales) {
if (linkLocale === locale) {
paths.push({ params: { slug: splittedSlug }, locale });
}
}
});
// pagination route generation on custom post types like posts and cases
const per_page = 10;
const startPagesArr = Object.values(data.links)
.map((obj) => obj)
.filter((obj) => obj.is_startpage == true)
.filter((obj) => obj.slug.split("/").length > 2);
// make a loop that loops through all startpages and fetches all stories that are children of that startpage
for (const startPage of startPagesArr) {
let res = await getStoryblokApi().get("cdn/links/", {
is_folder: false,
starts_with: startPage.slug,
paginated: 1,
page: 1,
per_page: per_page,
sort_by: "first_published_at:desc",
filter_query: {
component: {
in: "post,case,template",
},
},
});
let totalPages = Math.ceil(res.total / per_page);
let splittedSlug = startPage.slug.split("/");
const linkLocale = splittedSlug[0];
splittedSlug.shift();
if (splittedSlug == "") splittedSlug = false;
// ... Loop through locales and push the paginated pages to the paths Array
for (const locale of locales) {
if (linkLocale === locale) {
for (let i = 2; i <= totalPages; i++) {
paths.push({
params: {
slug: splittedSlug, // this is passed to the getStaticProps function
page: i, //this is not passed to the getStaticProps function
},
locale,
});
}
}
}
}
return {
paths: paths,
fallback: false,
};
}
Accessing the page query param in getStaticProps would solve the problem since I can pass that value to the API request and get the right blogposts to display on the right paginated pages.
Fetching data directly in the component is not preferable for SEO reasons since it will be client-side JS.
All the logic is for the whole site is in the pages/[[...slug.jsx]] file since there are multiple locales, but would it make sense to split it up so I have a dynamic file for the blog itself (across locales)?
I have tried returning the page query param in several different ways, but getStaticProps will only see the param that matches the filename (ex. params.slug will be accessible because the file is called [[...slug]].jsx].

How do I get translated column headers with Meteor and aldeed:tabular?

I'm running into the same problem as issue #53 of aldeed:tabular. When defining the table as suggested in the documentation, it is too soon to invoke a translation function (TAPi18n.__ or other), since the I18N variables are not yet set.
What is the nice, reactive way of feeding the translated column titles into DataTables, either directly as suggested by aldeed himself upon closing the issue, or through aldeed:tabular?
With .tabular.options
There is a way with the template's .tabular.options reactive
variable, but it is quirky. Here is a variation of the library
example using
tap-i18n to translate the
column headers:
function __(key) {
if (Meteor.isServer) {
return key;
} else {
return TAPi18n.__(key);
}
}
Books = new Meteor.Collection("Books");
TabularTables = {};
TabularTables.Books = new Tabular.Table({
name: "Books",
collection: Books,
columns: [] // Initially empty, reactively updated below
});
var getTranslatedColumns = function() {
return [
{data: "title", title: __("Title")},
{data: "author", title: __("Author")},
{data: "copies", title: __("Copies Available")},
{
data: "lastCheckedOut",
title: __("Last Checkout"),
render: function (val, type, doc) {
if (val instanceof Date) {
return moment(val).calendar();
} else {
return "Never";
}
}
},
{data: "summary", title: __("Summary")},
{
tmpl: Meteor.isClient && Template.bookCheckOutCell
}
];
}
if (Meteor.isClient) {
Template.tabular.onRendered(function() {
var self = this;
self.autorun(function() {
var options = _.clone(self.tabular.options.get());
options.columns = getTranslatedColumns();
self.tabular.options.set(_.clone(options));
});
});
}
With a forked version
I created a pull request against branch devel of meteor-tabular to enable the straightforward, reactive-based approach like so:
<template name="MyTemplateWithATable">
{{> tabular table=makeTable class="table table-editable table-striped table-bordered table-condensed"}}
</template>
var MyColumns = ["title", "author"];
// Assume translations are set up for "MyTable.column.title", "MyTable.column.author"
// in other source files; see TAPi18n documentation for how to do that
function makeTable() {
return new Tabular.Table({
name: "MyTable",
collection: MyCollection,
columns: _.map(MyColumns,
function(colSymbol) {
return {
data: colSymbol,
title: TAPi18n.__("MyTable.column." + colSymbol)
};
})
});
}
if (Meteor.isServer) {
// Called only once
makeTable();
} else if (Meteor.isClient) {
// Reactively called multiple times e.g. when switching languages
Template.MyTemplateWithATable.helpers({makeTable: makeTable});
}
Recent versions of aldeed:tabular allow to specify a function for setting the column titles.
import {TAPi18n} from 'meteor/tap:i18n';
TabularTables = {};
TabularTables.Departments= new Tabular.Table({
name: 'Departments',
collection: Departments,
responsive: true,
autoWidth: true,
stateSave: false,
columns: [
{data: "name", titleFn: function() {
return TAPi18n.__("name");
}},
{data: "description", titleFn: function() {
return TAPi18n.__("description");
}}
]
});
The language change is reactive. If you have translations you can switch and columns will be translated.
TAPi18n.setLanguage("en");
TAPi18n.setLanguage("de");
Word of warning:
This currently does not work when you include invisible columns in your table data. The offset is wrong and you get wrong column titles.

Publishing Users information but without "secret" fields

I am publishing multi-user information (using Meteor.users collection) for the purpose of naming posts creators and have their names and other small details associated with those posts, but I do NOT want to publish the complete documents for each user as they have "secret" login information.
Here is the code I am using:
Meteor.publish("serverforumthread", function(thread){
check(thread, String);
var replies = forumReplies.find({thread: thread});
var users = {};
replies.map(function(r){
users[r.owner] = r.owner;
});
var userids = _.map(users, function(value, key){ return value; });
var projectedFields = {_id:1, username:1, forumStats: 1, services: 0};
var usrs = Meteor.users.find({_id:{$in: userids}}, projectedFields);
var anyUpdateToUsers = false;
usrs.map(function(owner){
var changed = false;
if(!owner.username){
owner.username = owner.emails[0].address.split("#")[0];
changed = true;
}
//owner.forumStats = undefined;
if(!owner.forumStats){
owner.forumStats = {};
owner.forumStats.postCount = 0;
owner.forumStats.postLikes = 0;
owner.forumStats.title = "the newbie";
owner.forumStats.tag = "newbie";
owner.forumStats.img = "http://placehold.it/122x122";
changed = true;
}
if(changed){
anyUpdateToUsers = true;
Meteor.users.update({_id: owner._id}, {$set:{ forumStats:owner.forumStats }});
}
});
if(anyUpdateToUsers) // refresh it
usrs = Meteor.users.find({_id:{$in: userids}}, projectedFields);
usrs.map(function(owner){
console.log(owner);
});
return [replies, usrs];
});
As you can see, I am only interested in publishing relies (posts) for a thread and their associated users username and small forumStats, I want to keep the "services" key secret, as it contains details that should not be published.
A sample output of the "console.log":
{ _id: 'hoRYFbRkXXbHYm8Ty',
createdAt: Tue Jun 03 2014 16:25:42 GMT+0100 (WEST),
emails: [ { address: 'somemail#gmail.com', verified: false } ],
forumStats:
{ postCount: 85,
postLikes: 5,
title: 'the newbie',
tag: 'newbie',
img: 'http://placehold.it/122x122' },
services:
{ password: { srp: [Object] },
resume: { loginTokens: [Object] } } }
What am I doing wrong?
Thank you.
Have a look at the examples in the field specifiers section of the docs, and give this a try:
var projectedFields = {fields: {username:1, forumStats: 1}};
You'll get _id for free, and it will only include the other fields that you specify. Note that you can't mix inclusion and exclusion options, meaning you can't have both 0's and 1's.
If that doesn't work, let me know and I'll look more carefully.

$scope issue on ngGridEventEndCellEdit event when i rollback data

I probably misunderstood something but here is my problem on plunker.
I put the relevant code here anyway:
var app = angular.module('myApp', ['ngGrid']);
app.controller('MyCtrl', function($scope) {
var cellNameEditable =
'<cell-template model=COL_FIELD input=COL_FIELD entity=row.entity></cell-template>';
var cellNameDisplay =
'<div class="ngCellText" ng-class="col.colIndex()">{{row.getProperty(col.field)}}</div>';
$scope.myData= [{"id":1,"code":"1","name":"Ain"},{"id":2,"code":"2","name":"Aisne"},{"id":3,"code":"3","name":"Allier"},{"id":4,"code":"5","name":"Hautes-Alpes"},{"id":5,"code":"4","name":"Alpes-de-Haute-Provence"},{"id":6,"code":"6","name":"Alpes-Maritimes"},{"id":7,"code":"7","name":"Ardèche"},{"id":8,"code":"8","name":"Ardennes"},{"id":9,"code":"9","name":"Ariège"},{"id":10,"code":"10","name":"Aube"}];
$scope.gridOptions = {
data: 'myData',
multiSelect: false,
enableCellSelection: true,
enableRowSelection: false,
enableCellEditOnFocus: false,
rowHeight: 100,
columnDefs: [
{field:'id', displayName:'Id', visible: false},
{field:'code', displayName:'Code', enableCellEdit:true},
{
field:'name', displayName:'Name', enableCellEdit:true,
cellTemplate: cellNameDisplay,
editableCellTemplate: cellNameEditable
}
]
};
});
app.directive('cellTemplate', function () {
var cellTemplate =
'<div><form name="myForm" class="simple-form" novalidate>' +
'<input type="text" name="myField" ng-input="localInput" ng-model="localModel" entity="entity" required/>' +
'<span ng-show="myForm.myField.$error.required"> REQUIRED</span>' +
'localModel = {{localModel}} localInput = {{localInput}} entity = {{entity}}' +
'</form></div>';
return {
template: cellTemplate,
restrict: 'E',
scope: {
localModel:'=model',
localInput:'=input',
entity:'=entity'
},
controller: function ($scope) {
$scope.$on('ngGridEventStartCellEdit', function (event) {
console.log('cellTemplate controller - ngGridEventStartCellEdit fired');
$scope.oldEntity = angular.copy(event.currentScope.entity);
$scope.oldValue = angular.copy(event.currentScope.localModel);
});
$scope.$on('ngGridEventEndCellEdit', function(event) {
console.log('ngGridEventEndCellEdit fired');
if(event.currentScope.myForm.$valid) {
if(!angular.equals($scope.oldEntity, event.currentScope.entity)) {
alert('data saved !');
}
} else {
$scope.localModel = angular.copy($scope.oldValue);
$scope.localInput = angular.copy($scope.oldValue);
$scope.entity = angular.copy($scope.oldEntity);
}
});
}
};
});
Then explanations:
I have a ng-grid and based on the official example named "Excel-like Editing
Example" but with enableCellEditOnFocus option turned to false.
The cell "name" is defined in a directive containing a form to handle
data validation before updating the model.
I want to implement this behavior: When a user put invalid data, the
directive display error message and when the user leave the field, the
directive rollback data. If everything ok then I let the data updated.
The rollback part does not work. On the given plunker line 67 to 72 (last block on the code given here) it
fails to retore data. But my binding is with "=" so it should. Or maybe
because I am on the ngGridEventEndCellEdit event it breaks the links ?
I really don't understand why it fail.
So to reproduce my issue: enter in modification on a name cell, delete
all the data, REQUIRED is shown, then go out from the cell -> model is
not rolled back.
If you use a custom template, you should emit ngGridEventEndCellEdit event.

How do I make a collection reactive based on the contents of another?

In short, I want to do:
Meteor.publish('items', function(){
return Item.find({categoryId: Categories.find({active: true} });
});
The flag 'active' as part of 'Categories' changes regularly.
I also tried unsub/resub to the Items collection by leveraging reactivity on the Categories collections, and it works, unfortunately it re-triggers on ANY modification to the Categories collection, regardless if it affected the 'active' flag or not.
What are my options?
Nothing solved the issue of the items not being 'deleted' locally when the category is flagged as inactive on the server. Solution (ish) is to:
Client:
Categories.find({active: true}).observeChanges({
added: function(){
itemsHandle && itemsHandle.stop();
itemsHandle = Meteor.subscribe("items");
}
});
Server:
Meteor.publish('items', function(){
var category = Categories.findOne({active: true});
return category && Items.find({categoryId: Categories.findOne({active: true}._id);
});
I realize this isn't perfect (still uses client side code), but it works and its the cleanest I could think of. I hope it helps someone!
A possible solution is to create a dependency object, watch for all categories change, and trigger the dep change if the active flag was toggled. Something along these lines:
var activeCount = Categories.find({active: true}).count();
var activeDep = new Deps.Dependency();
Deps.autorun(function() {
var activeCountNow = Categories.find({active: true}).count();
if(activeCountNow !== activeCount) {
activeCount = activeCountNow;
activeDep.changed();
}
});
Meteor.publish('items', function(){
activeDep.depend();
return Item.find({categoryId: Categories.find({active: true} });
});
Note: I'm only verifying whether the number of active categories have changes so that I don't have to keep the active list in the memory. This may or may not be appropriate depending on how your app works.
Edit: Two-sided flavor mentioned in the comments:
Client:
var activeCount = Categories.find({active: true}).count();
var activeDep = new Deps.Dependency();
Deps.autorun(function() {
var activeCountNow = Categories.find({active: true}).count();
if(activeCountNow !== activeCount) {
activeCount = activeCountNow;
activeDep.changed();
}
});
Deps.autorun(function(){
activeDep.depend();
Meteor.subscribe('items', new Date().getTime());
});
Server:
Meteor.publish('items', function(timestamp) {
var t = timestamp;
return Item.find({categoryId: Categories.find({active: true} });
});
Meteor.startup(function() {
Categories.find().observe({
addedAt: function(doc) {
trigger();
},
changedAt: function(doc, oldDoc) {
if(doc.active != oldDoc.active) {
trigger();
}
},
removedAt: function(oldDoc) {
trigger();
}
});
});
Now, the trigger function should cause the publish to rerun. This time it's easy when it's on the client (change subscription param). I'm not sure how to do this on the server - perhaps run publish again.
I use the following publish to solve a similar issue. I think it is only the one line nesting of queries that limits the reactivity. Breaking one query out inside the publish function seems to avoid the issue.
//on server
Meteor.publish( "articles", function(){
var self= this;
var subscriptions = [];
var observer = Feeds.find({ subscribers: self.userId }, {_id: 1}).observeChanges({
added: function (id){
subscriptions.push(id);
},
removed: function (id){
subscriptions.splice( subscriptions.indexOf(id)) , 1);
}
});
self.onStop( function() {
observer.stop();
});
var visibleFields = {_id: 1, title: 1, source: 1, date: 1, summary: 1, link: 1};
return Articles.find({ feed_id: {$in: subscriptions} }, { sort: {date: -1}, limit: articlePubLimit, fields: visibleFields } );
});
//on client anywhere
Meteor.subscribe( "articles" );
Here is another SO example which gets the search criteria from the client through subscribe if you decide that is acceptable.
Update: Since the OP struggled to get this going I made a gist and launched a working version on meteor.com. If you just need the publish function it is as above.

Resources