Skip to content

View

The View is the most oftenly used class in the frontend. Every page is rendered by multiple view objects. Views can have child views. Child views can have their own child views and so on. When a parent view is rendered (by calling the render method), it generates a single HTML and adds it to the DOM. That HTML contains HTML of all child views. Any view can be re-rendered later.

A view file client/custom/src/views/test/my-custom-view.js:

// AMD module definition (ES modules are supported in ext-template). The first argument can be omitted.
// Names should be in a lower case. A hyphen to be used for word separation.
// The `custom:` prefix indicates to the loader that the base path is `client/custom/src`.
// A `my-module:` prefix would correspond to `client/custom/modules/my-module/src`.
define('custom:views/test/my-custom-view', ['view'], (View) => {

    // Extending from the base `view` class.
    return class extends View {

        // Optionally, define an extended constructor.
        constructor(options) {
            super(options);
        }

        // A template. See the separate article about templates.
        // language=Handlebars
        templateContent = `
            <div class="some-test-container">{{{someKeyName}}}</div>            
            <p>{{viewObject.someParam1}}</p>
            <p>{{someParam2}}</p>
            <p><a class="action" data-action="test">Test Action</a></p>            
            <div class="another-test-container"></div>
        `

        // Alternatively, a template can be defined in a separate file.
        // The `custom` prefix indicates that the base path is `client/custom/res/templates`.        
        //template = 'custom:test/my-custom-view'

        // Initializing. Called on view creation, the view is not yet rendered.
        setup() {
            // Calling the parent `setup` method, can be omitted.
            super.setup();

            // Instantiate some property.
            this.someParam1 = 'test 1';

            this.addHandler('focus', '.record input[data-name="hello"]', (event, target) => {
                // Do something.
            });

            // When we create a child view in the setup method, rendering of the view is held off
            // until the child view is loaded (ready), the child view will be rendered along with the parent view.
            // The first argument is a key name that can be used to access the view further.
            // The second argument is a view name.
            // The method returns a promise that resolves to a view object.
            this.createView('someKeyName', 'custom:test/my-custom-child-view', {
                // A relative selector of the DOM container.
                selector: '.some-test-container',
                // Or a full selector.
                //fullSelector: '#some-id',
                // Pass some parameter.
                someParam: 'test',
            }); 

            // Options passed from the parent view.
            console.log(this.options); 

            // A model can be passed from the parent view.
            console.log(this.model);

            // All event listeners are recommended to be initialized in the `setup` method.

            // Use listenTo & listenToOnce methods for listening to events of another object
            // to prevent memory leakage.

            // Subscribe to model change.
            // Subscribing with the `listenTo` method guarantees automatic unsubscribing on view removal,
            // so there won't be a memory leak.
            this.listenTo(this.model, 'change', () => {
                // Whether a specific attribute changed.
                if (this.model.hasChanged('someAttribute')) {
                    const value = this.model.get('someAttribute');
                }
            });

            // Subscribe to model sync (saved or fetched). Fired only once.
            this.listenToOnce(this.model, 'sync', () => {});

            // Subscribe to a DOM event. `cid` contains an ID unique among all views.
            // Requires explicit unsubscribing on view removal.
            $(window).on('some-event.' + this.cid, () => {});

            // Translating a label.
            const translatedLabel = this.translate('myLabel', 'someCategory', 'MyScope');
        }

        // Called after contents is added to the DOM.
        afterRender() {            
            // The view container (DOM element).
            console.log(this.element); 

            // Accessing a child view.
            const childView = this.getView('someKeyName');

            // Checking whether a view is set.
            const hasSomeView = this.hasView('someKeyName');

            // Destroying a child view, also removes it from DOM.
            this.clearView('someKeyName');

            // Initializing a reference to some DOM element.
            this.$someElement = this.$el.find('.some-element');
        }

        // Data to be passed to the template.
        data() {
            return {
                someParam2: 'test 2',
            };
        }

        // Called when the view is removed.
        // Useful for destroying event listeners initialized for the view.
        onRemove() {
            $(window).off('some-event.' + this.cid);
        }

        // A custom method.
        someMethod1(value) {
            // Create and render a child view.
            this.createView('testKey', 'custom:test/my-another-custom-child-view', {
                selector: '.another-test-container', 
                value: value,
            })
            .then(view => view.render());
        }

        someMethod2() {
            // To proceed only when the view is rendered.
            // Useful when the method can be invoked by the caller before the view is rendered.
            this.whenRendered().then(() => {
                // Do something with DOM.
            });
        }
    }
}

See more about templates.

See the source file of the view class.

Waiting for some data loaded before rendering

Sometimes we need to get some data loaded asynchronously before the view is rendered. For this purpose we can use the wait method inside the setup method.

The wait method can receive a promise:

setup() {
    this.wait(
        Promise.all([
            this.model.fetch(),
            this.model.collection.fetch(),
        ])
    );
}

The model factory returns a promise, so we can pass it to the view method:

setup() {
    this.wait(
        this.getModelFactory().create('Case')
            .then(model => {
                model.id = this.model.id;

                return model.fetch();
            })
            .then(data => {
                console.log(data);
            })
    );
}

Wait until a model is fetched. The fetch method returns a promise.

setup() {
    this.wait(
        this.model.fetch()
    );
}

Wait for multiple independent promises:

setup() {
    this.wait(
        this.model.fetch()
    );

    this.wait(
        Espo.Ajax.getRequest('SomeUrl')
    );
}
setup() {
    this.wait(
        Promise.all([
            this.model.fetch(),
            Espo.Ajax.getRequest('SomeUrl'),
        ])
    );
}

A simple way to wait:

setup() {
    // This holds off the rendering.
    this.wait(true);

    Espo.Ajax.getRequest('Some/Request')
        .then(response => {
            // This cancels waiting and proceeds to rendering.
            this.wait(false);                
        });
}

Note

Feel free to use the async/await syntax instead of explicit promises.

setup

Called internally on initialization. Put initialization logic here. Options passed by the parent view are available in this.options.

afterRender

Called internally after render. Put manipulation with DOM here.

onRemove

Called internally on view removal. Reasonable for unsubscribing.

createView

Creates a child view. When we create a child view in the setup method, rendering of the view is held off until the child view is loaded (ready), the child view will be rendered along with the parent view. The first argument is a key name that can be used to access the view further (with getView method). The second argument is a view name. The method returns a promise that resolves to a view object.

Arguments:

  • viewKey – a view key;
  • viewName – a view name (path);
  • options – options.

Standard options (all are optional):

  • selector – a relative CSS to the view selector (as of v7.3);
  • model – a model;
  • collection – a collection.

It's important that every view have their actual selector so that the application knows how to access them (for re-rendering).

clearView

Removes a child view.

Arguments:

  • viewKey – a view key.

getView

Get a child view by a key.

Arguments:

  • viewKey – a view key.

assignView

As of v7.5.

Assign a view instance as a child view.

Arguments:

  • viewKey – a view key;
  • view – a view instance;
  • selector – a relative CSS selector.
this.assignView('someKey', new SomeView(options), 'some-selector');

reRender

Re-renders a view. Usually, called from inside the view. Returns a promise resolved once rendering is finished.

Arguments:

  • forceboolean – force rendering if the view was not rendered before.

render

Renders a view. Should be called if the view is called not in the setup method (after the view is already ready or rendered). Returns a promise resolved once rendering is finished.

templateContent = `
    <div data-name="someName">{{{someKeyName}}}</div>
`

setup() {
    this.createView('someKeyName', 'custom:test/my-custom-child-view', {}); 
}

async actionShowModal() {
    const view = await this.createView('dialog', 'custom:test/my-modal-view', {selector: '[data-name="someName]'})

    this.listenToOnce(view, 'some-event', eventData => {
        console.log(eventData);

        this.clearView('dialog');
    });

    await view.render();
}

whenRendered

Returns the promise resolving when the view is rendered.

this.whenRendered().then(() => doSomethingWithDom());

data

The method is called internally when rendering. Should return a key => value data (Object.) that will be passed to a template.

addHandler

As of v8.0.

Adds a DOM event handler.

this.addHandler('click', 'selector', 'methodName');
this.addHandler('mousedown', 'selector', (event, target) => { ... });

Events

setup() {
    // Use this way only when the view subscribes to self.
    this.on(eventName, callback); // subscribe to self
    this.once(eventName, callback); // subscribe once
    this.off(eventName, callback); // unsubscribe

    // Use this way to subscribe to another object. Prevents memory leaking.
    this.listenTo(object, eventName, callback); // subscribe to another object
    this.listenToOnce(object, eventName, callback); 
    this.stopListening(object, eventName); // unsubscribe

    // Triggering event.
    this.trigger(eventName);  
    this.trigger(eventName, objectWithEventData); // passing data        
}

Multiple events can be specified separated by a whitespace.

Built-in events

  • after:render – after the view is rendered;
  • remove – when the view is removed (destroyed); use it for cleaning up.