Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What is the easiest (best) way to store edited newsletters on the backend instead of localstorage #73

Closed
bwl21 opened this issue Dec 5, 2015 · 45 comments
Labels

Comments

@bwl21
Copy link
Contributor

bwl21 commented Dec 5, 2015

What would be the easiest way to replace localstorage by a backend

As far as I can see, it requires following steps

It appears to me that the latter issue needs to be done in index.html as well as in mosaico respectively in editor.html. Could we find another appraoch, for example:

  • only one HTML - file
  • Mosaico can be embedded so there is no need for an extra editor.html
  • do save and load etc. outside of Mosaico, maybe by plugins (even if I did not fully understand how to write/use/remove the plugins)

Of course beyond these basic things, one could think of more advanced scenarios, but this might be discussed #50 resp. #59

@bago bago added the question label Dec 6, 2015
@acellam
Copy link

acellam commented Dec 7, 2015

I don't know if my solution is the best, i started hacking mosaico 2 weeks back and i made some improvements.
What i do is that i store the metadata data and template to the database using ajax against the unique id and user id from mosaico editor.
When needed for edititing, i retrieve and pass those details back to mosaico editor

@bwl21
Copy link
Contributor Author

bwl21 commented Dec 7, 2015

Did you do this in index.html? Do you have a repository or a gist to show how you did this? How do you handle "save" button of mosaico?

@acellam
Copy link

acellam commented Dec 7, 2015

Refer to @bago comment on #18 . I followed his advice.
For this to work i added another hash to the editor url to help me identify if it is a new template or old one.
check out gist:
https://gist.github.com/mistaguy/25ae3b8ec8205ea0f3e8

and

https://gist.github.com/mistaguy/1c3d9dc981061563e7c8

@bago
Copy link
Member

bago commented Dec 7, 2015

We probably can make this simpler by merging metadata+content in a single object and by providing a simple "storage" plugin solution for which "localstorage" is the bundled implementation but it should be easier to plug-in a custom implementation (most people will have to do that).

But I'm also happy that we didn't have anything "pluggable" so that some people have to put their hands in the code! I'm happy to see how other developers hack in the code and understand what they expect there ;-)

@jbalatero
Copy link

This is by far my implementation. I implemented the Save button as plugin. This implementation does not involve localstorage which means don't put hash in your url like: http://youurl.com/editor.html#hashere, just the http://youurl.com/editor.html. it's because mosaico automatically loads the localstorage plugin whenever they found a hash in the url. I hope this helps.

var plugins;
plugins = [
    // plugin for integrating save button
    function(viewModel) {
        var saveCmd = {
            name: 'Save', // l10n happens in the template
            enabled: ko.observable(true)
        };

        saveCmd.execute = function() {
            saveCmd.enabled(false);
            viewModel.metadata.changed = Date.now();
            if (typeof viewModel.metadata.key == 'undefined') {
                viewModel.metadata.key = 'generate your unique key here';
            }

            // This is the simplest for sending it as POST
            // append postData with csrfToken
            var postData = {
                csrf_token: 'yourCsrfValueHere', // this is only required if your back-end requires csrf token
                metadata: viewModel.exportMetadata()
                content: viewModel.exportJSON(),
                html: viewModel.exportHTML()
            };

            $.post( 'your url here', postData)
                .done(function() {
                    viewModel.notifier.success(viewModel.t('Successfully saved.'));
                })
                .fail(function(jqXHR, textStatus, error) {
                    console.log(textStatus);
                    console.log(error);
                    console.log(jqXHR);
                    viewModel.notifier.error(viewModel.t('Saving failed. Please try again in a few moment or contact us.'));
                })
                .always(function() {
                    saveCmd.enabled(true);
                }
            );

            // and this is the alternative by sending it as POST but the html content as blob.
            // you can ignore and remove this part.

            var formData = new FormData();
            formData.append('csrf_token', 'you csrf value here'); // this only applies if your need csrf token
            formData.append('metadata', viewModel.exportMetadata());
            formData.append('json', viewModel.exportJSON());

            // I intended to send the html as blob
            // JavaScript file-like object
            var html = new Blob([viewModel.exportHTML()], { type: 'text/html'});
            formData.append('html', html);

            // You can use the simple $.post() instead of this.
            $.ajax({
                url: 'you url here',
                type: 'POST',
                data: formData,
                processData: false,  // tell jQuery not to process the data
                contentType: false,   // tell jQuery not to set contentType
                beforeSend: function() {
                    viewModel.notifier.info(viewModel.t('Saving please wait...'));
                },
                success: function(){
                    viewModel.notifier.success(viewModel.t('Successfully saved.'));
                },
                error: function(jqXHR, textStatus, error) {
                    console.log(textStatus);
                    console.log(error);
                    console.log(jqXHR);
                    viewModel.notifier.error(viewModel.t('Saving failed. Please try again in a few moment or contact us.'));
                },
                complete: function() {
                    saveCmd.enabled(true);
                }
            });
        };

        viewModel.save = saveCmd;
    },
];

var ok = Mosaico.init(yourOptionsHere, plugins);

if (!ok) {
    alert('Something went wrong initializing the editor. Please try again or contact us!');
}

@notchris
Copy link

This feature alone would make this application 100x more useful.

@CaptainHypertext
Copy link

@jbaltero Thank you for posting that plugin, it is a lifesaver and super educational.

@bago
Copy link
Member

bago commented Jun 20, 2016

@jbaltero you can use Mosaico.start method instead of Mosaico.init if you don't want to enable the "localstorage" built-in plugin login.

@GLenotre
Copy link

Newbie dev here. How do I implement this plugin? Should I just replace the saveCmd.execute = function() in mosaico.js (lines 9907-9917)?

@CaptainHypertext
Copy link

CaptainHypertext commented Jun 20, 2016

@GLenotre I was just working on this myself. You're supposed to put it in your editor.html file where Mosaico gets initialized. See line 40 where the plugins variable is commented out. Put the plugins array right there, and it will be included when Mosaico is initialized.

BUT I was actually having trouble getting it to work. The save button still isn't firing the plugin function, and I'm getting no errors. I don't know if there's anything further you have to do to get it to fire beyond what's posted up there, but any help from @jbaltero or @bago would be infinitely appreciated.

@jbalatero
Copy link

@GLenotre , @CaptainHypertext is right, go check editor.html line 40 and you should be able to get how it was implemented.

@CaptainHypertext what do you mean by save button isn't firing the plugin function? You mean the ajax? Have you debugged it by a simple jquery alert();?

@CaptainHypertext
Copy link

Thanks for replying,
Yes I have been debugging it. Here's what I have, copied from yours and modified a little bit:

var plugins = [
// plugin for integrating save button
function(viewModel) {
    console.log('PROCESS PLUGIN');

    var saveCmd = {
        name: 'Save', // l10n happens in the template
        enabled: ko.observable(true)
    };

    saveCmd.execute = function() {
        console.log('FIRE SAVE');
        saveCmd.enabled(false);
        viewModel.metadata.changed = Date.now();
        if(typeof viewModel.metadata.key == 'undefined') {
            viewModel.metadata.key = 'abcd12345';
        }

        // This is the simplest for sending it as POST
        // append postData with csrfToken
        var postData = {
            csrf_token: '0YDRwO90s3MzlnBDOB3ISx0qZ9vpwREjIPCEGOZ8', // this is only required if your back-end requires csrf token
            metadata: viewModel.exportMetadata(),
            content: viewModel.exportJSON(),
            html: viewModel.exportHTML()
        };

        $.post('http://sm.localhost/template/save', postData)
            .done(function() {
                viewModel.notifier.success(viewModel.t('Successfully saved.'));
            })
            .fail(function(jqXHR, textStatus, error) {
                console.log(textStatus);
                console.log(error);
                console.log(jqXHR);
                viewModel.notifier.error(viewModel.t('Saving failed. Please try again in a few moments or contact us.'));
            })
            .always(function() {
                saveCmd.enabled(true);
            });
    };

    viewModel.save = saveCmd;
}
];
var ok = Mosaico.init({
imgProcessorBackend: basePath + '/image',
emailProcessorBackend: basePath + '/download',
titleToken: "MOSAICO Responsive Email Designer",
fileuploadConfig: {
  url: basePath + '/upload'
}
}, plugins);

Mosaico is loading just like normal, and when I click Save, it seems to just do the normal save function, and fires no ajax, and logs no FIRE SAVE. PROCESS PLUGIN logs though.

@jbalatero
Copy link

@CaptainHypertext Have you checked your URL? make sure it doesn't contain a hash character like http://youurl.com/editor.html#hashere.

@bago
Copy link
Member

bago commented Jun 20, 2016

If you don't want the "localStorage" plugin in the way just use Mosaico.start instead of Mosaico.init.

Mosaico.start({ yourmosaicooptions }, 'yourtemplatepath', undefined /* metadata */, undefined /* model */, extensions);

@CaptainHypertext
Copy link

CaptainHypertext commented Jun 20, 2016

Ah, yes, thanks guys. I think I'm starting to understand this a bit better.

Yes, I was still using the hash in the url. I removed that and started using the Mosaico.start function. It's firing the plugin function now.

On the other hand, I've lost the Test and Download buttons (I can live without them for now, though).

@bago
Copy link
Member

bago commented Jun 20, 2016

The other buttons are enabled by the "localStorage" plugin that you disabled.
Have a look at how that plugin implement the 2 buttons and do something similar in your own plugin:

https://github.com/voidlabs/mosaico/blob/master/src/js/ext/localstorage.js#L25

@bago
Copy link
Member

bago commented Jun 20, 2016

PS: I admit the test and download buttons do not belong to the localstorage plugin... we need some refactoring to sort this stuff out. I'm on a bigger issue right now but I hope I will be able to put some order here too in the next months.

@CaptainHypertext
Copy link

Awesome, thanks for the link, I appreciate the help!

@GLenotre
Copy link

GLenotre commented Jun 20, 2016

I'm using @CaptainHypertext code above, but I get this error: "Uncaught TypeError: Cannot read property 'template' of undefined". The reference is to template-loader.js:160. Template-loader.js at line 160 says templateCompiler = function(performanceAwareCaller, templateUrlConverter, templateName, templatecode, jsorjson, metadata, extensions, galleryUrl)

I should add that none of the JS appears on myurl.com/editor.html, just a background color..

@GLenotre
Copy link

GLenotre commented Jun 23, 2016

I'm still having trouble implementing the plugin. In particular, when I navigate to myurl/editor, I get kicked back to myurl. What's wrong with my code below after var plugins = []; in editor.html? What should I be changing if anything in mosaico.js?

var ok = Mosaico.start( { imgProcessorBackend: basePath + '/image', emailProcessorBackend: basePath + '/download', titleToken: "MOSAICO Responsive Email Designer", fileuploadConfig: { url: basePath + '/upload' } }, './', undefined, undefined, plugins );

Thanks in advance!

@bago
Copy link
Member

bago commented Jun 23, 2016

If you are reimplementing the storage then you'll have to put some login in your editor.html so to load the model+metadata and pass this information instead of "undefined"s in your Mosaico.start call.

The way you are starting mosaico you are not telling Mosaico what template to use and what content to start with.

@notchris
Copy link

I wrote a backend in PHP/MySQL that can save, load and modify Template data / metadata. I also re-implemented features that were originally dependent on localstorage. Message me if you would like the source, I'm not too familiar with GitHub so I'm not sure exactly how to share it on here.

@emu42
Copy link

emu42 commented Jul 20, 2016

Thanks for the sample code. I have written a backend for a Java web application to store newsletters there and load them again.

One thing I am still missing though is a way to name these templates. I am thinking a little dialog box like when sending a test mail would be the simplest way to implement this. Has anybody done this before?

@jbalatero
Copy link

jbalatero commented Jul 20, 2016

@emu42 What I did is: the mosaico is in an iframe and the parent contains a textfield for name. It's just a quick fix though not directly on mosaico.

@emu42
Copy link

emu42 commented Jul 20, 2016

I also have Mosaico inside an IFrame, but placing the text field for the name outside it seems like an awkward solution. So I'm currently looking through the Mosaico code and hoping to figure out how to do this...

@emu42
Copy link

emu42 commented Jul 20, 2016

Looking at the email prompt, I now have a few lines of code like this:

var templateName = global.localStorage.getItem("templateName");
templateName = global.prompt(viewModel.t("Template name"), templateName);

However the global variable is apparently undefined, resulting in a JavaScript ReferenceError. How do I get a reference to this global object from within a plugin?

@bago
Copy link
Member

bago commented Jul 20, 2016

global = window

@emu42
Copy link

emu42 commented Jul 20, 2016

So simple when you know it... ;) Thanks! I added the name to the POST data. Seems to work fine.

@notchris
Copy link

Here is a PHP/MySQL solution that saves / loads the template data from the backend. You still need to build Mosaico normally, but this example should help some people out. I also migrated Testing / Exporting away from localStorage. PHPMailer is also used in place of NodeMailer. You can find my fork here: Mosaico + Template Backend

@ShahVivek
Copy link

@jbaltero @CaptainHypertext @notchris

I saw your soultions to save email template , I implemented plugins code in my editor.html and use Mosaico.start method. But 'Save' button is not displaying on my editor.

here is my code,

var plugins;
plugins = [
    // plugin for integrating save button
    function (viewModel) {
        var saveCmd = {
            name: 'Save', // l10n happens in the template
            enabled: ko.observable(true)
        };
        var downloadCmd = {
            name: 'Download', // l10n happens in the template
            enabled: ko.observable(true)
        };
        saveCmd.execute = function () {
            saveCmd.enabled(false);
            viewModel.metadata.changed = Date.now();
            if (typeof viewModel.metadata.key == 'undefined') {
                var rnd = Math.random().toString(36).substr(2, 7);
                viewModel.metadata.key = rnd;
            }
            // This is the simplest for sending it as POST
            // append postData with csrfToken
            var postData = {
                csrf_token: 'yourCsrfValueHere', // this is only required if your back-end requires csrf token
                metadata: viewModel.exportMetadata(),
                content: viewModel.exportJSON(),
                html: viewModel.exportHTML()
            };
            $.post('your url here', postData)
                .done(function () {
                    viewModel.notifier.success(viewModel.t('Successfully saved.'));
                })
                .fail(function (jqXHR, textStatus, error) {
                    console.log(textStatus);
                    console.log(error);
                    console.log(jqXHR);
                    viewModel.notifier.error(viewModel.t('Saving failed. Please try again in a few moment or contact us.'));
                })
                .always(function () {
                    saveCmd.enabled(true);
                }
            );
            // and this is the alternative by sending it as POST but the html content as blob.
            // you can ignore and remove this part.
            var formData = new FormData();
            formData.append('csrf_token', 'you csrf value here'); // this only applies if your need csrf token
            formData.append('metadata', viewModel.exportMetadata());
            formData.append('json', viewModel.exportJSON());
            // I intended to send the html as blob
            // JavaScript file-like object
            var html = new Blob([viewModel.exportHTML()], { type: 'text/html' });
            formData.append('html', html);
            // You can use the simple $.post() instead of this.
            $.ajax({
                url: 'you url here',
                type: 'POST',
                data: formData,
                processData: false,  // tell jQuery not to process the data
                contentType: false,   // tell jQuery not to set contentType
                beforeSend: function () {
                    viewModel.notifier.info(viewModel.t('Saving please wait...'));
                },
                success: function () {
                    viewModel.notifier.success(viewModel.t('Successfully saved.'));
                },
                error: function (jqXHR, textStatus, error) {
                    console.log(textStatus);
                    console.log(error);
                    console.log(jqXHR);
                    viewModel.notifier.error(viewModel.t('Saving failed. Please try again in a few moment or contact us.'));
                },
                complete: function () {
                    saveCmd.enabled(true);
                }
            });
        };
        viewModel.save = saveCmd;
        viewModel.download = downloadCmd;
    },
];
var ok = Mosaico.start({
    imgProcessorBackend: basePath + '/img/',
    emailProcessorBackend: basePath + '/dl/',
    titleToken: "Email Editor",
    onSave: function (saveObject) { alert('hi'); },
    fileuploadConfig: {
        url: basePath + '/upload'
    }
}, 'templates/versafix-1/template-versafix-1.html', undefined, undefined, plugins);  
if (!ok) {
    console.log("Missing initialization hash, redirecting to main entrypoint");
    //document.location = ".";
}
});

I know I have to change in saveCmd.execute and $.post but right now my problem is my editor don't even show save button.
Any suggestion will be helpful

@bago
Copy link
Member

bago commented Aug 10, 2016

Do you see the download button? Does everything else works? Do you see errors in the browser javascript console?

Add a console log after "viewModel.download = downloadCmd;" so you know that the code is executed.

@notchris
Copy link

notchris commented Aug 10, 2016

@ShahVivek
I can confirm my template backend implementation is working, attempt building that using the modified files and let me know if you're still having trouble.

@ShahVivek
Copy link

@notchris I checked your code , but actually i am implementing it in .net.

@ShahVivek
Copy link

ShahVivek commented Aug 11, 2016

@bago I am not able to see download button. Everything else is works fine. I added console log after "viewModel.download = downloadCmd;" and it prints successfully.
But yes there are some errors in browser javascript console. I am trying to solve it and get you back.
Here is list of errors.

Missing initialization hash, redirecting to main entrypoint default.html:133:5
translateTemplate: timer started mosaico.min.js:1427
Cannot apply default value to variable when using expressions mosaico.min.js:1391:1079
ERROR: found unexpected -ko- attribute in compiled template anonymous-4586 , you probably mispelled it: Array [ "data-ko-display", "-ko-attr-width" ] mosaico.min.js:1422:3796

ERROR: found unexpected -ko- attribute in compiled template sideArticleBlock-show , you probably mispelled it: Array [ "-ko-attr-width" ] mosaico.min.js:1422:3796

translateTemplate: 282.61ms mosaico.min.js:1427
generateModel: timer started mosaico.min.js:1427
generateModel: 8.09ms mosaico.min.js:1427
generateEditors: timer started mosaico.min.js:1427
TODO EDITOR ignoring longTextStyle.linksColor property because it is not used by the template prop: linksColor type: styler level: 2 preheaderBlock mosaico.min.js:1373:4703
TODO WARN Missing label for object  theme mosaico.min.js:1373:5340
generateEditors: 37.88ms mosaico.min.js:1427
initializeViewmodel: timer started mosaico.min.js:1427
initializeViewmodel: 1.28ms mosaico.min.js:1427
This is my log after viewModel.download = downloadCmd line  default.html:120:9
applyBindings: timer started mosaico.min.js:1427
applyBindings: 1015.37ms mosaico.min.js:1427

@bago
Copy link
Member

bago commented Aug 11, 2016

You probably altered the template code and broke it: "found unexpected -ko- attribute in compiled template sideArticleBlock-show". Don't mix too many changes if you want to be able to debug. So, try altering the save button without altering the rest of mosaico or the template.

@notchris
Copy link

If I take the time to write a tutorial for implementation with different backends, can it be posted on the main page?

@bago
Copy link
Member

bago commented Aug 11, 2016

Can't promise anything before I see the result.
Of course if I think it will be useful I'll be happy to add it to the wiki:
https://github.com/voidlabs/mosaico/wiki/Serving-Mosaico

@mattghall
Copy link

I have successfully implemented a full PHP backend with saving and loading all done server side. I will issue a pull-request next week after some more testing.

@webportpl
Copy link

Do you have this backend?

@janober
Copy link

janober commented Sep 15, 2016

+1 Also interested in the php backend with storage

@notchris
Copy link

My example is a working php backend with storage @janober @webportpl

@kattapug
Copy link

@mattghall I will be interested in who you successfully implemented a full PHP backend with saving and loading all done server side.

@gordon-matt
Copy link
Contributor

When I try using the .start() method instead of the init() method, I get the following error:
Missing initialization hash, redirecting to main entrypoint

and tracked it back to this error:
Error: Unable to process binding "template: function(){return 'main' }"
Message: Cannot find template with ID main

My code is as follows:

plugins = [
    // plugin for integrating save button
    function (viewModel) {
        var saveCmd = {
            name: 'Save',
            enabled: ko.observable(true)
        };

        saveCmd.execute = function () {
            saveCmd.enabled(false);
            viewModel.metadata.changed = Date.now();
            if (typeof viewModel.metadata.key == 'undefined') {
                var rnd = Math.random().toString(36).substr(2, 7);
                viewModel.metadata.key = rnd;
            }

            // This is the simplest for sending it as POST
            var postData = {
                //csrf_token: 'yourCsrfValueHere', // this is only required if your back-end requires csrf token
                metadata: viewModel.exportMetadata(),
                content: viewModel.exportJSON(),
                html: viewModel.exportHTML()
            };

            $.post('/mosaico/save', postData)
                .done(function () {
                    viewModel.notifier.success(viewModel.t('Successfully saved.'));
                })
                .fail(function (jqXHR, textStatus, error) {
                    console.log(textStatus);
                    console.log(error);
                    console.log(jqXHR);
                    viewModel.notifier.error(viewModel.t('Saving failed. Please try again in a few moments or contact us.'));
                })
                .always(function () {
                    saveCmd.enabled(true);
                });
        };

        viewModel.save = saveCmd;
    },
];

var ok = Mosaico.start({
    imgProcessorBackend: basePath + '/img/',
    emailProcessorBackend: basePath + '/dl/',
    titleToken: "MOSAICO Responsive Email Designer",
    //onSave: function (saveObject) { alert('hi'); },
    fileuploadConfig: {
        url: basePath + '/upload/'
    }
}, '/templates/versafix-1/template-versafix-1.html', undefined, undefined, plugins);

Any help much appreciated

@athulhere
Copy link

athulhere commented Feb 8, 2019

Here is a PHP/MySQL solution that saves / loads the template data from the backend. You still need to build Mosaico normally, but this example should help some people out. I also migrated Testing / Exporting away from localStorage. PHPMailer is also used in place of NodeMailer. You can find my fork here: Mosaico + Template Backend

You are just awesome . Did the most of it and that was a quick starter.

@notchris
Copy link

notchris commented Feb 8, 2019

@athulhere Glad it helped! I haven't updated it in years so I'm happy it is still working.

@bago bago mentioned this issue Sep 16, 2020
@voidlabs voidlabs locked and limited conversation to collaborators Apr 27, 2022
@bago bago converted this issue into discussion #636 Apr 27, 2022

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
Projects
None yet
Development

No branches or pull requests