Sulu tutorials

How to develop a bundle in the Sulu-Admin – #4: News-Form

This part of the tutorial is based on the results of the previous blog-post.

Part 4 is about building a simple form to create and edit the “News”-Item. This form communicates with the RESTful-API that we talked about in part three.

Add-Form

To get a new container for our form component we have to define a route in the main.js ("Resources/public/js/main.js") file of our bundle.

// file: Resources/public/js/main.js

app.sandbox.mvc.routes.push({
    route: 'example/news/add',
    callback: function() {
        return '<div data-aura-component="news/form@examplenews"/>';
    }
});

Behind this route we will create a component in the folder "Resources/public/js/components/news/form". This will be our first fully functional component. To initialize the standard elements of the Sulu-Admin we use the hooks from the AdminBundle which will be explained more detailed in the Sulu documentation. There are two very important hooks which initialize the content container (where our component will be started) and the header. These hooks are simple properties of the component.

// file: Resources/public/js/components/news/form/main.js

define(function() {
    return {

        header: {
            title: 'news.headline',
            toolbar: {
                buttons: {
                    save: {
                        parent: 'saveWithOptions'
                    }
                }
            }
        },

        layout: {
            content: {
                width: 'fixed',
                leftSpace: true,
                rightSpace: true
            }
        },

        initialize: function() {
            this.$el.html('<h1>Hello awesome sulu world</h1>');
        }
    };
});

This code initializes a header with a simple toolbar which contains the standard save button. The title of our module will also be displayed. For this we use a translation key which will be translated to all languages of the Sulu-Admin (we will talk about this in the final part of the tutorial). The second hook we use is the layout-hook which initializes a container with a fixed width and space on the left and right.

To see the current state of the tutorial simply go to Sulu-Admin and use the url-hash "#example/news/add".

In a different file we define the HTML structure of the form.

<!-- file: Resources/public/js/components/news/form/form.html -->

<form id="news-form" class="grid">
    <div class="form-group grid-row">
        <label for="news-title"><%= translations.title %></label>
        <input type="text" id="news-title" name="title" class="form-element input-bold required" data-mapper-property="title" data-validation-required="true"/>
    </div>
    <div class="form-group">
        <label for="news-content"><%= translations.content %></label>
        <textarea id="news-content" name="content" class="form-element" data-mapper-property="content"></textarea>
    </div>
</form>

This is a simple form which contains an input field for the title and a textarea for the content. The additional attributes "data-mapper-property" and "data-validation-required" define the target property of the model. We need this attributes later to set to or get data from the form. This structure and CSS classes ensure the same style and layout as other standard Sulu-Bundles.

To render the form we use another JavaScript-hook defaults from the AdminBundle. This component is able to handle default values for options, translations and - as we need it - the templates. These templates can contain underscore template tags. As an example for this take a look at the template we have created.

// file: Resources/public/js/components/news/form/main.js

define(['text!./form.html'], function(form) {

    return {

        defaults: {
            templates: {
                form: form
            },
            translations: {
                title: 'public.title',
                content: 'news.content'
            }
        },

        header: {
            title: 'news.headline',
            toolbar: {
                buttons: {
                    save: {
                        parent: 'saveWithOptions'
                    }
                }
            }
        },

        layout: {
            content: {
                width: 'fixed',
                leftSpace: true,
                rightSpace: true
            }
        },

        initialize: function() {
            this.render();
        },

        render: function() {
            this.$el.html(this.templates.form({translations: this.translations}));
        }
    };
});

For those who do not know RequireJS, the first line defines that the file "form.html" should be loaded before the component and then injected into the constructor (we will talk about translating the keys for title and content in the final part of this tutorial).

One of the core-features of the Sulu-Admin is the data-mapper which is able to map objects to forms and vice-versa. It uses the dom-attributes "data-mapper-*" and "data-validation-*" to initialize a simple mapping for objects. This feature can simply be initialized by calling the function "this.sandbox.form.create('#news-form')" with the root-selector of our form. Additionally we add a simple change detection to enable the save-button when you change the values of the form.

// file: Resources/public/js/components/news/form/main.js

initialize: function() {
    this.render();

    this.bindDomEvents();
},

render: function() {
    this.$el.html(this.templates.form({translations: this.translations}));

    this.sandbox.form.create('#news-form');
},

bindDomEvents: function() {
    this.$el.find('input, textarea').on('keypress', function() {
        this.sandbox.emit('sulu.header.toolbar.item.enable', 'save');
    }.bind(this));
}

Now we can respond to the “Save” button (which will create an Aura event on click), get the data from our form and save them with our news API.

// file: Resources/public/js/components/news/form/main.js

defaults: {
    templates: {
        form: form,
        url: '/admin/api/news<% if (!!id) { %>/<%= id %><% } %>'
    },
    translations: {
        title: 'public.title',
        content: 'news.content'
    }
},

initialize: function() {
    this.render();

    this.bindDomEvents();
    this.bindCustomEvents();
},

bindCustomEvents: function() {
    this.sandbox.on('sulu.toolbar.save', this.save.bind(this));
},

save: function(action) {
    if (!this.sandbox.form.validate('#news-form')) {
        return;
    }

    var data = this.sandbox.form.getData('#news-form');

    this.sandbox.util.save(this.templates.url({id:null}), 'POST', data).then(function(response) {
        this.afterSave(response, action);
    }.bind(this));
},

afterSave: function(response, action) {
    this.sandbox.emit('sulu.header.toolbar.item.disable', 'save');

    if (action === 'back') {
        this.sandbox.emit('sulu.router.navigate', 'news');
    } else if (action === 'new') {
        this.sandbox.emit('sulu.router.navigate', 'news/add');
    } else if (!this.options.id) {
        this.sandbox.emit('sulu.router.navigate', 'news/edit:' + response.id);
    }
}

This code does the following things: 

  • It responds to the “Save” button event "sulu.toolbar.save"
  • When the event was emitted the values will be validated and extracted from the form
  • This values will be saved with the news API
  • After that the save action will be handled.

The edit form and the data list are not implemented at this stage. So don't be confused when there is no content on the routes.

Edit-Form

To enable the editing in the form we only have to do this:

  1. Add an edit route in the main.js file
  2. Handle the id option within the form component

The first is very easy to achieve:

// file: Resources/public/js/main.js

app.sandbox.mvc.routes.push({
    route: 'news/edit::id',
    callback: function(id) {
        return '<div data-aura-component="news/form@examplenews" data-aura-id="' + id + '"/>';
    }
});

In the component we now an use "!!this.options.id" to determine if it is a edit or an add form. With the id we can load the data from the API in the function "loadComponentData" which is also a hook from the admin bundle.

render: function() {
    this.$el.html(this.templates.form({translations: this.translations}));

    this.form = this.sandbox.form.create('#news-form');
    this.form.initialized.then(function() {
        this.sandbox.form.setData('#news-form', this.data || {});
    }.bind(this));
},

save: function(action) {
    if (!this.sandbox.form.validate('#news-form')) {
        return;
    }

    var data = this.sandbox.form.getData('#news-form'),
        url = this.templates.url({id: this.options.id});

    this.sandbox.util.save(url, !this.options.id ? 'POST' : 'PUT', data).then(function(response) {
        this.afterSave(response, action);
    }.bind(this));
},

loadComponentData: function() {
    var promise = $.Deferred();

    if (!this.options.id) {
        promise.resolve();

        return promise;
    }
    this.sandbox.util.load(_.template(this.defaults.templates.url, {id: this.options.id})).done(function(data) {
        promise.resolve(data);
    });

    return promise;
}

The "loadComponentData" method must return a promise. If such a method is specified, the AdminBundle delays the startup of your component and sets "this.data" with your loaded data (where this is the context of your component). So when your component-initializing-method is called you can easily access your data via "this.data" and don’t have to worry about asynchronicity. This data will be used to set the values of the form in the render method.

To see the difference between add and edit look at this commit.

Now we are able to store and load single news-items. That's it for the fourth part of the series. Next time we will create the data list and finish our NewsBundle.

The code for this part of the tutorial can be found here.