CityWebConsultants / Iris

Modular content management and web application framework built with Node.js and MongoDB
http://irisjs.org
Other
9 stars 7 forks source link

New Handlebars-powered embed system (with universal liveupdating) #342

Closed FilipNest closed 7 years ago

FilipNest commented 7 years ago

This is a breaking change but will hopefully be worth it. I've updated core to work but any embeds in site templates like forms, menus and tags will need changing to the new syntax to work with this.

Outcomes:

Here's a guide to how the new Handlebars-powered Iris embed system introduced in this pull request works.

Basics

Each embed is now put in using the following syntax:

Inline embeds


{{{iris embed="embedType" param1="value" param2=current.eid}}}

Triple brackets work in the same way as usual in Handlebars. If it's an embed that creates HTML, you'll need to skip the escaping by using three brackets.

Block embeds

Some embeds (currently only entity embeds but more like menus can come in the future) support block type embedding.


{{#iris embed='embedType' param1='1' as |result|}}

    {{#each result}}

        {{this.title}}

    {{/each}}

{{/iris}}

Note the as parameter that lists what the embed's return value's variable name within the block will be. This can then be used as normal inside the Handlebars block.

Parameters

Parameters to an embed can be passed in the following ways.

A liveupdate parameter is available for use on any embed type. Currently only the entity type uses it but any type can easily be converted to use live updating. The parameter sets everything up, all that's needed on the server side is a message to live update an embed template.

Liveupdate API on the server side

Each time a websocket connection is made with the Iris server on a page with embeds marked with the liveupdate parameter, a registration message is sent to the server registering a listener on the embed for that current page view. As soon as the socket disconnects the embed listener is unregistered.

All the currently registered embed listeners are stored in an object called iris.modules.frontend.globals.liveEmbeds keyed as an object by the embed type.

This contains information about the embed (all its settings) and the authPass and socketID of the user that registered a listener for it.

It has a sendResult() function that allows you to refresh the embed for the client. If you just want to check the result, or alter it before you send it to the client there's a getResult() function.

Permissions

Each embed stores the authPass of the user that registered it so the embed, if reloaded, is reloaded with the same permissions (If these permissions could have changed between the page load and the update event, an authPass can be sent to the sendResult() update function on each embed listener).

Example update message function

Here's an example that updates all entity embeds across the system for every connected client:


var updateEmbeds = function () {

  if (iris.modules.frontend.globals.liveEmbeds.entity) {

    Object.keys(iris.modules.frontend.globals.liveEmbeds.entity).forEach(function (embed) {

      iris.modules.frontend.globals.liveEmbeds.entity[embed].sendResult();

    })

  }

};

Examples for current embed types

Forms

Forms need a formID parameter. Any other parameters will be passed into the form render hooks.


{{{iris embed="form" formID="login"}}}

Tags

Tags have a name parameter for the tag container name and an exclude parameter that takes a JSON array of tags to exclude from the container.


{{{iris embed='tags' name='headTags' exclude='["socket.io"]'}}}

Messages

A user's messages can be shown with:


{{{iris embed="messages"}}}

Menu

Menus take a menu parameter and an optional template parameter that can override the menu template.


{{{iris embed='menu' menu="admin_toolbar"}}}

Template

Template embeds simply take the template lookup you want to use to embed a template.


{{{iris embed='template' template="sidebar"}}}

Region

Region embeds take a single region parameter.

{{{iris embed="region" region="sidebar"}}}

Entity

Entity embeds are used as block embeds. They take the same parameters as an entity fetch hook on the server side.

Additionally you can use loadscripts=true to load the fetched entity into a live-updating client side entity database for use with systems such as Angular.js . If this is used you can use the variableName parameter to store a friendly name for the clientside database collection.

As mentioned above liveupdate=true will make the embed's contents automatically update if entity content changes in the database.


{{#iris embed='entity' variableName='myEntities' loadscripts=true liveupdate=true entities='["comment"]' queries='[{"field":"title","operator":"contains","value":"hello"}]' as |list|}}

  {{#each list}}

    {{this.title}}

  {{/each}}

{{/iris}}
adam-clarey commented 7 years ago

Your description doesn't match what you have changed in the code, for example to embed a form you say to do:

{{{iris embed="form" formID="login"}}}

but for mail settings its:

{{{iris 'form' '{"formID":"mailSettings"}'}}}

FilipNest commented 7 years ago

That's a shortcut that made changing core easier. Both methods work. I can replace every instance with the longer method if needed and then take out the shortened one from the code. I have a list of all core forms so shouldn't be too tough to do. Unless keeping the shortened version in is a good idea. It's fairly harmless.

adam-clarey commented 7 years ago

Not sure which is the better approach, im always dubious whenever i see hardcoded variables like embed="form" or formID="login" etc which are more likely to become depreciated and are less flexible than json.

I'm also seeing random bits like 'hook_entityview3' on line 301 of entity_fetch.js what does view3 mean?

Can you also explain the querycache?

This is such a massive change that is going to break everything, im struggling to see where the real benefits are. And now that we have actual client projects going live we just cant afford to keep making such big breaking changes. Is it possible to make it backwards compatible so that we can change to the new way?

FilipNest commented 7 years ago

Thanks for looking at this. I know it's huge!

Regarding old/new syntax. Before we had:

[[[form login,{"hello":"world"}]]]

which passed the hello parameter to a form with a formID of login. We also had lots of forms and other embeds that were passing multiple comma separated parameters: sometimes strings, sometimes JSON.

With this we can do the same thing with keyed parameters/attributes. Their order no longer matters and we can use Handlebars helpers within them. They can even be grouped together in one embedOptions parameter which is a JSON string of all options (like we had before). They're not less flexible than JSON because they're all full JSON strings (objects, arrays, booleans...).

The entity fetch/views stuff should probably go out. I've merged this through my Master branch which has been running this code for a few weeks now. hook_entity_view3 is a typo so I'll certainly fix that. (fixed). The query cache thing is a flag only function I put in when testing the performance of it. It's only activated when you add it to your launch config but I should probably take it out and merge back in afterwards.

Backwards compatibility could be achieved by some regex that replaces all of the old style embeds with the new ones. A module could maybe do that by hooking into hook_frontend_template and attempting to replace all the old embeds on runtime. I'd expect it to be very messy but it is possible. Find and replace in a code editor may be easier for projects jumping to the new syntax.

The main advantages of this new system:

Using Handlebars helpers for embeds seems like a much better option than packaging in our own like we were. It wasn't possible before but now the new Handlebars helpers Promises library mean we can do async helpers leaving no need to use our own system.

I'm running this code on my own fork for my projects at the moment but will leave this PR branch as is to work changes into for when you think it's ready to merge in. That way I can still make other Iris changes to a branch branched off this one but this will still be easy enough to merge in once you think it's ready.

Pop in any suggestions for improvements to this thread.