Механика работы

Базовая структура виджета выглядит следующим образом:

./
├── i18n  
|  └── ru.json  
|  
├── images  
├── logo_min.png  
├── logo_main.png  
|  └── logo.png  
|  
├── templates  
|  └── page.twig  
|  
├── manifest.json  
├── style.css  
└── script.js
Файл Описание
manifest.json Файл в форматеjson, содержащий описание виджета, настройки виджета, параметры виджета, выводимые пользователю, локализации, с которыми работает виджет.
script.js JS-файл, который будет подключен на стороне пользователя в указанных в manifest.json областях.
style.css CSS-файл стилей виджета, виджет обязательно должен для своих главных элементов использовать уникальный класс и всем дочерним элементом менять стили каскадом относительно базового класса, чтобы не конфликтовать с системными элементами и другими виджетами.
images/ Папка для размещения файлов изображений, которые используются в виджете. Должна содержать в себе 3 файла в формате (png, jpeg,jpgилиgif), которые будут использоваться как логотип виджета в разных областях видимости. Размер каждого файла не должен превышать300 КБ. logo_min.png и logo_medium.png — обязательно, если виджет работает в карточках, используется во всех списках и карточках контактов или сделок в свернутом и развернутом состоянии соответственно. Также необходимо загрузить logo.png для поддержки старых версий.
i18n/ Папка, содержащая файлы локализаций в формате ключ:значение. На текущий момент возможно использование трех локализаций: русской (ru), английской (en) и испанской (es). Все переводы будут доступны в JS.
templates/ Необязательная папка, может содержать twig-шаблоны, если они используются в виджете.

Рассмотрим простейший пример виджета, он представляет из себя JS файл, следующего содержания:

define(['jquery'], function($) {
  'use strict';

  return function() {
    var self = this;

    this.callbacks = {
      render: function() {
        return true;
      },
      bind_actions: function() {
        return true;
      },
      init: function() {
        return true;
      },
      destroy: function() {
        // ...
      },
      settings: function() {
        // ...
      },
      onSave: function() {
        return true;
      }
    };

    return this;
  };
});

При заходе, например, в карточку сделки будет выполнена следующая цепочка вызовов:

  1. сначала будет вызван колбэк render, который должен обязательно в итоге вернуть true
    • в этом колбэке виджет должен отрисовать свои необходимые элементы (например, собственный блок в правой колонке виджетов в карточке, используя метод this.render_template)
  2. одновременно с ним вызывается колбэк bind_actions, в нем виджет может добавить необходимые слушатели событий как на собственные элементы, которые только что были отрисованы, так и на какие-то элементы в системе
  3. следом за ними будет вызван колбэк init, в котором можно делать какие-то действия, когда мы уверены, что виджет уже отрисовал все необходимое и все обработчики тоже готовы к работе

Все три колбэка обязательно должны присутствовать в объекте this.callbacks, без них виджет не будет функционировать в указанных областях видимости.

Колбэк destroy будет вызван при покидании текущей области видимости (к примеру, при выходе из карточки сделки, если у виджета была область подключения lcard).

Виджет самостоятельно должен хранить свое состояние, то есть в какой области видимости он сейчас проинициализировался, чтобы отрабатывать правильную логику в своих колбэках. Делать это можно на основе AMOCRM.getBaseEntity() и AMOCRM.isCard(), подробнее про глобальные системные переменные можно почитать здесь.

Объект виджета также наследует системный интерфейс объекта Widget, который имеет методы, представленные дальше.

Не стоить забывать, что работая через this внутри колбеков, вы не сможете обращаться к методам объекта Widget. Поэтому мы создаем переменную self, но вы можете использовать любой другой удобный для вас способ. Чтобы лучше понять принцип работы, советуем почитать статью о this и о замыканиях.

self.system();

Данный метод необходим, для того, чтобы получить системные данные. Данные возвращаются в виде объекта

Параметр Описание
area Область на которой воспроизводится виджет в данный момент
amouser_id Id пользователя
amouser Почта пользователя

Пример ответа:

{
  area: "ccard" ,
  amouser_id: "103586" ,
  amouser: "testuser@amocrm.ru"
}

self.set_settings();
Метод set_settings() позволяет добавлять виджету свойства.

//создается свойство с именем par1 и значением text
self.set_settings({ par1: "text" });

// в ответ придет объект с уже созданным свойством
self.get_settings();

self.get_settings();

Данный метод возвращает настройки, которые были сохранены пользователем через модальное окно виджета.

self.set_status();

Виджет может иметь один из трех статусов. Статус виджета отображен в области settings, на иконке виджета. В случае, если виджет использует данные, введенные пользователем для API стороннего сервиса, и эти данные введены неверно, то можно использовать данную функцию для отображения статуса error.

Доступны статусы install(виджет не активен) и installed (виджет активен), error (виджет в состоянии ошибки). Пример вызова:

self.set_status('error');

self.get_install_status();

Данный метод вернет статус установки виджета. Данные возвращаются в виде строки. Возможные значения – installed (виджет установлен), install (виджет не установлен), not_configured (тур виджета пройден, но настройки не заполнены).

self.i18n(‘userLang’);

Возвращает объект с языковыми сообщениями из json-файла текущей локали, по ключу переданному в аргументе.

self.get_version();

Возвращает текущую версию из манифеста, обычно используется для подгрузки чего-либо из сети в качестве идентификатора кеша для браузера, чтобы при обновлении виджета из сети подтягивался актуальный файл.

self.get_accounts_current();

Возвращает данные по аккаунту без запроса в API и асинхронности (результат не полностью совпадает с ответом API, если нет необходимых данных, то без запроса все-таки не обойтись).

self.crm_post(host, data, callback, type);

Отправляет POST-запрос на внешний сервис, пропуская запрос через amoCRM. Принимает следующие аргументы:

  • host – url на который надо отправить запрос
  • data – данные, которые отправляем
  • callback – выполняется в случае успеха
  • type – тип возвращаемых данных (по умолчанию text)

Данный метод не может выполняться чаще одного раза в секунду, если вызовы будут чаще, то отправка запроса будет откладываться на время соразмерное время. То есть если отправить одновременно 5 запросов, то пятый отправится только через 5 секунд. Все остальное время он будет ожидать своей очереди.

self.render();

Тут остановимся подробнее, у данного метода есть несколько вариантов использования.

Начать стоит с того, что в системе мы используем twig в качестве основного шаблонизатора, поэтому, если ранее с ним не сталкивались, советуем ознакомиться с документацией. В качестве js реализации работает библиотека https://github.com/twigjs/twig.js.

Первый вариант, и тут мы можем шаблон передать строкой и сразу его отрендерить, выглядеть это будет так:

self.render({
  data: 'The {{ baked_good }} is a lie.'
}, {
  baked_good: 'cupcake'
});

В результате этого вызова получим строку: "The cupcake is a lie.". Данный метод иногда может пригодиться, но все-таки он не очень удобен из-за того, что шаблон нам по сути надо хранить в виде строки прямо в script.js. Лучше шаблоны отделять от логики виджета и класть в отдельные twig-файлы, таким образом ваш виджет не превратится не нагроможденное месиво из всего подряд.

Поэтому рассмотрим следующие два варианта, они как раз позволяют нам вынести шаблоны в отдельные файлы. Создадим рядом с файлом script.js файл template.twig и перепишем наш render в такой вид:

self.render({
  href: '/template.twig',
  base_path: self.params.path,
  v: self.get_version(),
  load: function (template) {
    template.render({
      baked_good: 'cupcake'
    });
  }
});

В данном случае мы подгрузим из сети файл template.twig, после чего отрисуем его. Важный момент, как можно заметить, теперь для того, чтобы шаблон отрисовать надо сначала его загрузить, поэтому метод стал асинхронным.

Ну и третий вариант, в нем отличие от третьего только в том, что с render мы можем работать как с объектом типа promise, поэтому вызов может выглядеть вот так:

self.render({
  href: '/template.twig',
  base_path: self.params.path,
  v: self.get_version(),
  promised: true
}).then(function () {
  template.render({
    baked_good: 'cupcake'
  });
});

Так как у вас может быть много шаблонов, чтобы каждый раз не прописывать кучу параметров в вызове метода render можно воспользоваться следующим модулем (templates.js):

define(function() {
  'use strict';

  var instance = null;

  return function(context) {
    if (!instance && context) {
      instance = context;
    }

    // вспомогательный хелпер для
    // работы с шаблонами, нужно вызвать
    // один раз с контекстом виджета

    return function(template) {
      return instance.render({
        href: '/templates/' + template + '.twig',
        base_path: instance.params.path,
        v: instance.get_version(),
        promised: true
      });
    };
  };
});

Script.js при этом может выглядеть вот так:

define(['./templates.js'], function(createTemplatesRenderer) {
  'use strict';

  return function() {
    var self = this;

    // создаем инстанс
    var getTemplate = createTemplatesRenderer(self);

    this.callbacks = {
      init: function() {
        return true;
      },

      render: function() {
        getTemplate('users').then(function(template) {
          // делаем что-то с template
        });

        return true;
      },

      bind_actions: function() {
        return true;
      }
    };

    return this;
  };
});

Тут мы подключили модуль templates.js к своему виджету, создали инстанс шаблонов и в методе render вызов getTemplate(‘users’) подгрузит файл users.twig из папки templates виджета и в случае успеха передаст его в then.

Мы хотим, чтобы виджеты выглядели как можно более нативно, поэтому предусмотрели возможность отрисовки стандартных контролов amoCRM в шаблонах виджетов, ниже приведены возможные для использования контролы:

  • textarea
  • suggest
  • select
  • radio
  • multiselect
  • date_field
  • checkbox
  • checkboxes_dropdown
  • button
  • input

Для использования данных контролов внутри шаблонов виджета предусмотрена специальная функция в twig include_control.

Пример с рендерингом шаблона input:

{{ include_control('input', {
  class_name: 'widget_custom_class_name',
  name: 'widget_input',
  placeholder: 'Поле для заполнения',
  value: 'Сохраненное значение'
}) }}

Первый параметр (строка) – код шаблона контрола #TEMPLATE_NAME#.

Второй параметр (объект) – параметры, которые принимает контрол.

Подробное описание параметров каждого шаблона и примеры использования вы сможете найти в нашем сторибуке.

self.render_template()

Данный метод используется для отрисовки блока виджета в правой панели в карточке и списках. Пример вызова с возможными параметрами:

self.render_template({
  caption: {
    class_name: 'js-zoom-caption',
    html: '<image src="' + self.params.path + '/images/logo.png?v=' + self.get_version() + '"/>'
  },
  body: '<link type="text/css" rel="stylesheet" href="' + self.params.path + '/style.css?v=' + self.get_version() + '">',
  render: '<div class="zoom-form">' +
    '<div class="zoom-form-button create_meeting">' + lang['buttonText_createMeeting'] + '</div>' +
    '<div class="zoom-copy-link zoom-text"></div>' +
    '<div class="zoom-copy-start-link zoom-text"></div>' +
    '<div class="zoom-copy-password zoom-text"></div>' +
  '</div>'
});

self.get_current_card_contacts_data()

Данный метод не имеет сетевых запросов и предоставляет полезные данные о контактах в области карточки. А именно:
в карточках сделки, компании, покупателя предоставляются данные о связанных контактах, а в карточке самого контакта предоставляются данные этого контакта.

Данные возвращаемые методом:

      [
        {
          id: number,
          name: string, 
          first_name: string, 
          last_name: string, 
          phones: [], 
          emails: []
        }
      ];

Метод возвращает промис.

this.$authorizedAjax()

Метод отправки ajax-запроса с временным токеном авторизации для текущего пользователя. В запрос добавляется заголовок X-Auth-Token, удаленный сервер должен разрешить получение запросов с домена аккаунта (настроить CORS). Наследует все входящие параметры функции $.ajax, в ответ также возвращает полностью совместимый с ответом метода $.ajax объект типа $.Deferred. Пример вызова:

define([], function() {
  'use strict';

  return function() {
    var self = this;

    this.callbacks = {
      init: function() {
        return true;
      },
      render: function() {
        self.$authorizedAjax({
          url: 'https://example.com/'
        }).done(function (response) {
          console.log('success', response);
        }).fail(function (err) {
          console.log('error', err);
        });
        return true;
      },
      bind_actions: function() {
        return true;
      }
    };
    return this;
  };
});

self.listenSocketChannel(channel_name, array_keys, callback)

Данный метод используется для подписки на сокет и прослушивания определенных сообщений, которые появляются в канале. Метод возвращает функцию отписки от канала.
Принимает следующие аргументы:

  • channel_name (строка) – название канала, который хотим прослушивать
  • array_keys (массив) – массив ключей для фильтрации, если сообщение имеет данные ключи, то оно будет доступно после фильтрации (по умолчанию пустой массив [])
  • callback (функция) – функция обработки сообщений сокета (через параметр функции – message передается payload сообщений, которые прошли фильтрацию исходя из array_keys)

Доступные каналы для прослушивания:

Канал Описание
notifications:${ACCOUNT_ID}:${USER_ID} События по нотификациям
inbox:talks:${ACCOUNT_ID} События по беседам
system:state:${ACCOUNT_ID} События по системе
${ENTITY}:card:${CARD_ID} События по карточкам сущности
dashboard:events События dashboard
${ENTITY}:pipeline:${PIPELINE_ID} События pipeline
${ENTITY}:events События сущности

Пример подписки:

const channel_name = `${entity[entity_type]}:card:${entity_id}`;
const array_keys = ['body', 'payload', 'data', 'params'];

const callback = (message) => {
  const path = message.data.params;

  if (path.old_value) {
    console.log(path.old_value);
  }
};

self.subscription = self.listenSocketChannel(channel_name, array_keys, callback);

Пример отписки:

self.subscription();