Custom Layouts Plugin


(Angus McLeod) #1

Repository: GitHub - angusmcleod/discourse-layouts

Example screenshot:


Superficially, this plugin is similar to existing Discourse sidebar plugins, i.e.:

Dynamic Sidebar Plugin
Discourse Sidebar Blocks
Simple Sidebar Theme
Curated Home Plugin
Sidebar Plugin (widget driven)

However its code and purpose are quite different. This plugin is intended to provide the infrastructure for custom Discourse layouts, the primary custom layout being the addition of sidebars. It does this by changing how the client works, rather than making changes with CSS, jQuery or template overrides.

I initially wrote the core code of this plugin for my own purposes. It got the point when I thought others may find it useful. If you’ve ever spent any time with the Discourse code you’ll know that the client route logic, particularly for the discovery routes, is not simple. Customizing that logic is complex. This plugin abstracts that complexity so you can focus on developing user features.

It is important to understand that this plugin is infrastructure for other plugins. Most features of this plugin will not be useful to most people out of the box.

Please also note that this plugin is currently desktop only. All of the features mentioned below only affect desktop clients.

Sidebars

This plugin adds sidebars to the app by wrapping existing templates with sidebars on a route-by-route basis. It doesn’t override any templates. Without getting too much into the weeds, the way this works is by using Ember’s {{partial}} template helper to retain the existing scope of the route, while adding an extra template wrapping that route template.

This means that you have granular control over what appears where, without losing any normal Discourse functionality or any future updates to that functionality. You can turn the left and right sidebars on or off individually for each route. You can have a left sidebar for the Latest discovery route with no category, then a right sidebar for the Top discovery route with a category. Or you could have both sidebars for a specific category. You can also turn each sidebar off and on for topics. The setting interface for this is the same as the setting interface for Topic List Previews. The settings in Admin > Settings > Plugins allow you to turn the sidebars off/on for the non-category discovery routes, the categories discovery route, globally for all category routes and for topics.

The settings in Category Settings > Settings allow you to turn the sidebars off / on for each discovery route in that category.

Sidebar Widgets

The Layouts plugin does not include any sidebar widgets itself. I am also releasing two widgets for this plugin:

Profile Widget
Map Navigation Widget

If you want to use either widget, just add them to your instance like you would any other plugin. They will then appear as options for left and right widgets (respectively) in Admin > Settings > Plugins and Category Settings > Settings:

One of the problems that any sidebar plugin faces is that different people want different content (or ‘widgets’) in a sidebar. If the sidebar infrastructure and widgets are in the same plugin this means that any specific feature requirements need to be either done in the core sidebar plugin or in a forked version. This approach is not extensible. By abstracting the sidebar infrastructure from the ‘widgets’ / sidebar content, anyone can create their own sidebar widgets and continue to take advantage of the core sidebar infrastructure used by others.

I do not anticipate that these two widgets (profile and map navigation) will fit everybody’s use cases. Generally speaking, I will also not be taking requests, or commissions, for specific feature widgets. What this plugin does is make it much easier for anyone to add or commission their own sidebar widgets. Adding your own sidebar widget is now a matter of creating a single Virtual DOM widget and adding some styling. Any javascript developer can create their own powerful custom layouts using this plugin for the infrastructure.

How to create your own sidebar widget

  1. Create a plugin with a Virtual DOM widget. The single widget in the Profile Widget is a useful guide.
  • If your widget is rendered in a category discovery route, the relevant category object will be available in the widget as an attribute. See the Map Navigation widget for an example of this. If your widget is rendered in a topic route, the relevant topic object will be available in the widget as an attribute. See the Profile widget for an example of this. See further here.

  • I suggest that, like the profile widget does, use the class widget-container in the tagName so that your widget has a consistent container style.

  1. In your plugin.rb file add your widget name to the sidebar setting choices for the relevant side you want to use the widget on. For example, the profile widget can be used in the left sidebar because it is added to the left sidebar choices like so:
after_initialize do
  SiteSetting.class_eval do
    @choices[:layouts_sidebar_left_widgets].push('profile')
  end
end
  1. Your new widget will now appear in the Site and Category widget choices (see above).

Generally, the Profile Widget is the best guide to follow for adding your own widget.

The Map Navigation Widget provides an entirely different Discovery navigation interface for Discourse by hooking up a leaflet.js map to Discourse categories. If you upload a .geojson file in Category Settings > Settings > JSON uploader and toggle Category has geojson, the area represented by the coordinates in the geojson file will appear on the map in blue if it is a parent category and in orange if it’s a child category. If you click on the added area on the map you will navigate to the associated category. I’ve built this because tying categories to physical locations is necessary for what I personally want to do with Discourse (at some point). If someone else finds this particular way of navigating a forum useful then all for the better. I don’t anticipate that this widget will fit many use cases, but you never know… In any event, it’s a useful example of a more complex widget working with the Layouts plugin.

Discovery List Elements

One of the problems you encounter if you add sidebars to Discourse discovery lists is that the navigation and list headers don’t work so well. That can be fixed with CSS, however you may also wish to change how the discovery navigation / headers actually work, in which case more direct changes are needed. This plugin gives you various ways to change each Discovery topic list, each of which work by changing what templates are actually rendered on the route (see further here), rather than by CSS or jQuery.

  1. layouts list navigation disabled. This setting removes the navigation bar from the selected topic list by preventing the navigation template from rendering on the relevant route.

  2. layouts list header disabled, This setting removes the list header from the selected topic list by preventing the list header from rendering on the relevant route.

  3. layouts list nav menu. This setting converts the nav pills (i.e. “Latest”, “New” etc) into a dropdown menu). This considerably reduces the amount of horizontal space required for the navigation buttons, which is necessary if you add sidebars.

Each setting works on an individual list basis, like the sidebar settings. This means you can have, say, a header and no nav for Latest with no category, and a nav menu and no header for New in Category A. You could use this plugin just to add / remove elements of the discovery lists, without adding any sidebars.

Further Development

As this plugin is about providing a custom layout infrastructure, further work will be to add more such infrastructure. It will not be to build specific widgets. As explained above, each widget is a separate plugin. If you’re not technically inclined and want your own custom content in a sidebar widget, please post the job on the marketplace.


[Paid] Widgets for the custom layouts plugin
Theming / Skinning Discourse to show Topics in a Responsive "Newsy" way
Bookmark Widget Plugin
Question Answer Plugin
How to edit this area?
Is there CSS code to put HTML banner beside post topic
Some things I've built which you can use
Custom Layout Widget, Im Buy
Topic list sidebar navigation
Support of sidebar plugins
Plugin with sidebar: how to modify small-width, medium-width, large-width?
Problem is installing plugin
Optional blog-like “homepage” for the forum
Support of sidebar plugins
Adding rss feed to discourse page
Locations Plugin
New per-category view mode: 2 columns; left is for topic list; right is for topic itself
Where can I find the template for navigation?
(Dante) #2

This is exactly what I need , thank you very much :+1:


(Daniela) #3

@angus it would be possible to test some functionality in a sandbox to see it in action?
With your other plugins of course.


(Jez) #4

This is very cool. I’ve had a play with Discourse Layouts and written dl-custom-content.

It’s a tiny plugin using Discourse Layouts to show arbitrary HTML.

A really awesome projects; thank you!


(Angus McLeod) #5

Great! I’m just in transit at the moment. I’ll check it out when I get back home next week.

And provide an example instance @trash


(Angus McLeod) #6

You can now see this plugin in action, along with my other plugins at: https://dev.civll.com/


#7

Great idea for a widget, I tried it on a test server (with a heavily modified Discourse) but I had a failed to bootstrap, is it working on yours still ?

The profile widget worked well during install.


(Angus McLeod) #8

@jez Thanks again for making a widget that works with Custom Layouts. I’ve submitted a PR to your widget repo to fix the issue @Steven mentions.


(SMHassanAlavi) #9

@angus
Thanks for you plugin.
I have some question about your widget.
in your profile widget there is some lines :

h('h3', this.attach('link', {
            route: 'user',
            model: currentUser,
            className: 'user-activity-link',
            icon: 'user',
            rawLabel: username
          }))

or

this.attach('button', {
          label: "sign_up",
          className: 'btn-primary sign-up-button',
          action: "sendShowCreateAccount"
        })

I know what these codes do but I don’t know where how to modify these codes.
for example I want to add a create topic button to widget but I don’t know how should I change
this code:

this.attach('button', {
          label: "sign_up",
          className: 'btn-primary sign-up-button',
          action: "sendShowCreateAccount"
        })

I know what is className(used for CSS) and label and rawLabel(used for translation)(if I am wrong please tell me) but I don’t know where should I find all other actions for button and icons.
I have another question,
In this code:

h('h3', this.attach('link', {
            route: 'user',
            model: currentUser,
            className: 'user-activity-link',
            icon: 'user',
            rawLabel: username
          }))

what does the route and model do?what is link?
Last question, in this code:
contents.push("img", "")
how should I config the image source?image is not in my site


(Angus McLeod) #10

Hey @Alavi1412, if I’m understanding you correctly, you want to modify the profile widget to add a button that allows the user to open the composer to create a new topic?

Any button has two basic parts two it: the button element, and the button action. I think you already understand the button element part. As you say you’ll need to use the button widget with the appropriate classes, icon and label (note that the button widget doesn’t have a rawLabel, only a link does - go to the button widget file and search for ‘rawlabel’ and there will be no matches. Yes the ‘label’ requires a translation object). We’ll also need the button action, which refers to a method that handles what happens when the button is clicked.

This is the process I would take to figure out what we need for both the button element and the button action. I suggest you follow these steps yourself, as it will help you understand other issues you’ll encounter.

As always, the place we start is with the existing Discourse functionality we want to copy. In this case we start with the button in normal Discourse that opens the new topic compose when it is clicked.

  1. Go to meta.discourse.org and highlight the Create Topic button with the chrome inspector. You’ll see it has the id create-topic.

  2. Do a search for create-topic in the Discourse code. These are the results you’ll get

    How do we know which results are relevant? There’s often good hints in the name of the files. We know that the ‘Create Topic’ button appears in the same part of the app as the navigation controls, so it seems likely that the files with ‘navigation’ in the path are relevant. Those results also seem to be about button elements, which is what we’re after.

  3. Click through one of those relevant results and you’ll find button elements in templates that give us much of the information we need. e.g.:

    <button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i>{{i18n 'topic.create'}}</button>
    

    We can simply copy / past these details to our button widget like so:

    this.attach('button', {
        className: 'btn btn-default',
        action: 'createTopic',
        label: 'topic.create',
        icon: 'plus'
      })
    

    Once we add that button widget to the profile widget, we now have a create topic button in the profile widget. We’re half the way there.

  4. If you click that new button, you’ll see a message in the console createTopic not found. We need to deal with the second part of the button, the button action. If you’ve read the part of the Ember Guide that covers Template Actions, you’ll know that the action handler we got from one of those templates refers to an action method in a corresponding component, controller or route. So let’s just do a search for that handler and find the corresponding instance.

    The search for createTopic will produce many results. It is useful to know that the part of the app with topic lists is called ‘discovery’. Also keep in mind that we’re looking for a method; any of the results are not methods. Scanning the results list the first one that really jumps out at me is this one (it’s both a method and it seems to be in the right place in the code:

    If you click through that result you’ll see a method like this:

    createTopic() {
      this.openComposer(this.controllerFor("discovery/topics"));
    },
    
  5. Ok we’ve now found the method. How do we get that method to work for our widget button? If you’ve read A tour of how the Widget (Virtual DOM) code in Discourse works, you’ll know that each action in a widget needs a corresponding action handler in the widget. Ok, so let’s create a createTopic method in the profile widget that does the same thing the createTopic method in the Discovery Route does.

    If you follow where the openComposer method in createTopic in the Discovery Route comes from (or open any of the other results in our search for createTopic methods in the codebase) you’ll figure you that what we need to do is send an ‘open’ action to the composer controller with the right parameters.

    this.controllerFor('composer').open({
      categoryId: controller.get('category.id'),
      action: Composer.CREATE_TOPIC,
      draftKey: controller.get('model.draft_key'),
      draftSequence: controller.get('model.draft_sequence')
    });
    

    So we need to access the composer controller in our widget action. The createTopic method in the discovery route also uses the discovery/topics controller, so we’ll need that too. If you look at the other action handlers in the profile widget you’ll see how they access controllers and routes from the widget. i.e.

    const cController = this.register.lookup('controller:composer');
    const dtController = this.register.lookup('controller:discovery/topics');
    

    Using this approach we can now make our own createTopic method.

      createTopic() {
        const cController = this.register.lookup('controller:composer');
        const dtController = this.register.lookup('controller:discovery/topics');
        cController.open({
          categoryId: dtController.get('category.id'),
          action: Composer.CREATE_TOPIC,
          draftKey: dtController.get('model.draft_key'),
          draftSequence: dtController.get('model.draft_sequence')
        });
      },
    

    The only part we’re missing now is the Composer object. That’s a reference to the composer model. All we need to do for that is import the composer model. Add the import at the top of the profile widget file

    import Composer from 'discourse/models/composer';
    

Once you add that new createTopic method and reload, your Create Topic button will now (mostly) work like the Create Topic button in the normal Discourse discovery. The composer will open. There are some issues that arise by virtue of the fact that the profile widget can appear in different contexts from the context in which the normal Create Topic button appears.

There are other ways to do the same thing we just did, however the key point here is:

  1. Start with what you already know. We know that there is a button in normal Discourse that does what we want to do. Start by figuring out how that works.

  2. Copy as much as possible. We can copy from both normal Discourse and the existing code in the profile widget that does similar things to what we want to do.

Using this approach the answers you get will not be 100% perfect, but they will get you 90% of the way there. Once you’re 90% there you have a much better idea of what the 10% of actual problems you’ll need to figure out are.

I have to get back to work now, so I’ll leave those other questions with you for now. Apply the same approach and see if you can figure out the answers.


(SMHassanAlavi) #11

@angus
Thank you so much.It helps me very well,
but I still have one question.
when our widget become long(longer than browser height), the button part is not shown after scrolling
before scrolling:


after scrolling:

one part of my widget is at the bottom but because of the height it is not shown even after scrolling.
in the other words, I don’t want my widget to float with user,I want to fix the widget in a position


(Angus McLeod) #12

Yup, there’s a setting for that actually. Check Settings > Plugins.


(SMHassanAlavi) #13

@angus
Why widget is not shown in categories page like :
http://localhost:3000/c/30
there is a white space in the widgets place but there is no widget


(Angus McLeod) #14

Have you enabled the sidebar on category pages?

You can enable the sidebar either for all category pages in the admin plugin settings or you can enable the sidebar and widgets for specific categories in the category settings.


(SMHassanAlavi) #15

Yes I have done this.but nothing is shown.
this page can define my problem


(Angus McLeod) #16

Ah, you mean the sidebars are not working on the tags index page?


(Angus McLeod) #17

I’ve added sidebar support to the tags show route. Let me know if that solves your issue.

You’ll need to turn sidebars on for the tags route in admin settings


(SMHassanAlavi) #18

@angus Thank you for your change but my problem was something else.(apologize for my delay, I was travelling)
I have done every changes you said but I still have this problem:


my widget didn’t show here.
Here is my code.


(Angus McLeod) #19

The problem is that you’re trying to convert the attrs object to a JSON string and it’s failing. This exception is preventing the sidebars from loading on category routes.

Change these lines

alert(path);
alert("state: " + JSON.stringify(state));
alert("attrs: " + JSON.stringify(attrs));

to this

console.log(path);
console.log("state: ", state);
console.log("attrs: ", attrs);

And check the output in your browser console: View > Developer > JavaScript Console

Also, rather than using an ajax call to get your category list, I suggest you use:

const categories = Discourse.Category.list()

Let me know if you need any help with the rest of it.


(SMHassanAlavi) #20

awesome !!!

where should I find something like this?
in writing widget I find the data I need in site and then in XHR of network tab in chrome, I find the url and I use this url in my widget and its response in ajax. but your way is very fast and useful. How can I replace my way of getting data by ajax with something like Discourse.Category.list()