= Components Components are the core contract between external xref:develop:modules.adoc[modules] and the core. They are used to define pieces of functionality that are pluggable to participatory spaces and can be enabled or disabled by the administrator. == Creating a new component If you want to create a new component, you can use https://github.com/decidim/decidim/tree/develop/decidim-generators[decidim-generators] to automatically generate a decidim component skeleton, or copy the basic structure of an existing mantained plugin. [source,console] ---- decidim --component engine_name ---- Components are just gems with one or more Rails engines included in it. You can use as an example https://github.com/decidim/decidim/tree/develop/decidim-pages[decidim-pages]. Check out the `lib/decidim/pages` folder: It includes several files, the most important of which is `component.rb`. Upload the component to GitHub with the naming *decidim-module-engine_name*, so it is easier to find on the https://github.com/decidim/decidim/network/dependents[dependency graph]. == Defining a component manifest Components are defined in a manifest, along with its engine and admin engine counterpart. There is a DSL available to describe all this: [source,ruby] ---- # :my_component is the unique name of the component that will be globally registered. Decidim.register_component(:my_component) do |component| # The user will be redirected to the component's engine when accessing it through # the public page of a participatory space. A component's engine is isolated # from the outside so it can deal with its own dependencies without having to # know its render path or its parent resources. component.engine = MyComponent::Engine # A component's admin engine will get rendered on the admin panel and follows # the same principles as the engine. It is isolated from the outside and # does not care about external dependencies. It only needs to care about its # underlying `component`. component.admin_engine = MyComponent::AdminEngine # Component hooks get called whenever relevant lifecycle events happen, like # adding a new component o destroying it. You always get passed the instance # so you can act on it. Creating or destroying a comoponent is transactional # along with its hooks, so you can decide to halt the transaction by raising # an exception. # # Valid hook names are :create and :destroy. component.on(:create) do |component| MyComponent::DoSomething.with(component) end # Export definitions allow components to declare any number of exportable files. # # An export definition needs a unique name, a collection, and a Serializer. If # no serializer is provided, a default, naive one will be used. # # Exports are then exposed via the UI, so the implementer only needs to care # about the export definitions. component.exports :component_resources do |exports| exports.collection do |component| MyComponent::Resource.where(component: component) end exports.serializer MyComponent::ResourceSerializer end # Import definitions allow data to be imported into a component. # # For now supported formats for imports are CSV, JSON and Excel (.xlsx). # Every resource type needs it is own creator, which creates resource from # parsed data. You also need to define specific messages for the importer and # can provide some extra options, too. component.imports :component_resources do |imports| # Define a form partial if you want to add custom fields to the form # imports.form_view = "decidim/my_component/admin/imports/component_resources_fields" # Define a custom form class name if you want to customize the form object # imports.form_class_name = "MyComponent::Admin::CustomImportForm" # Define UI messages for the import functionality imports.messages do |msg| # The resource name is used in the success message: # "X your resources successfully imported" (the "your resources" part) msg.set(:resource_name) { |count: 1| I18n.t("decidim.my_component.admin.imports.resources.component_resources", count: count) } # The title is the message shown at the top of the import page msg.set(:title) { I18n.t("decidim.my_component.admin.imports.title.component_resources") } # The label is what is shown in the import dropdown menu msg.set(:label) { I18n.t("decidim.my_component.admin.imports.label.component_resources") } # The optional help message can be used to provide additional contextual # help for the import file, e.g. how it should be formatted # msg.set(:help) { I18n.t("decidim.my_component.admin.imports.help.component_resources") } end # Define the creator that is used to create resources from the import data imports.creator MyComponent::ResourceCreator # Define an example data set to guide the user how to format the import # file. This will be available for download in the import view for each # supported import format. imports.example do |import_component| organization = import_component.organization [ organization.available_locales.map { |l| "title/#{l}" } + organization.available_locales.map { |l| "body/#{l}" }, organization.available_locales.map { "Example title" } + organization.available_locales.map { "Example body" }, organization.available_locales.map { "Example title 2" } + organization.available_locales.map { "Example body 2" }, ] end end end ---- Every model in a component does not have to (and should not) know about its parent participatory space, but instead should be scoped to the components. == Settings Components can define settings that modify its behavior. This settings can be defined to be set for the whole life of the component (global settings), or to be set for each different step of the participatory space (step settings). Each attribute defined can be described through properties: * they should have a `type`: `boolean`, `integer`, `string` (short texts), `text` (long texts) or `enum`. * they can be `required` or not * they can have a `default` value * `text` and `string` attributes can be `translated`, which will allow admin users to enter values for every language. * `text` attributes can use an `editor` to edit them as HTML code * `enum` attributes should have a `choices` attributes that list all the possible values. This could be a lambda function. * they can be `readonly` in some cases, throught a lambda function that received the current component within the `context`. [source,ruby] ---- # :my_component is the unique name of the component that will be globally registered. Decidim.register_component(:my_component) do |component| ... component.settings(:global) do |settings| settings.attribute :a_boolean_setting, type: :boolean, default: true settings.attribute :an_enum_setting, type: :enum, default: "all", choices: %w(all one none) end component.settings(:step) do |settings| settings.attribute :a_text_setting, type: :text, default: false, required: true, translated: true, editor: true settings.attribute :a_lambda_enum_setting, type: :enum, default: "all", choices: -> { SomeClass.enum_options } settings.attribute :a_readonly_setting, type: :string, readonly: ->(context) { SomeClass.readonly?(context[:component]) } end ... end ---- Each setting should have one or more translation texts related for the admin zone: * `decidim.components.[component_name].settings.[global|step].[attribute_name]`: Admin label for the setting. * `decidim.components.[component_name].settings.[global|step].[attribute_name]_help`: Additional text with help for the setting use. * `decidim.components.[component_name].settings.[global|step].[attribute_name]_readonly`: Additional text for the setting when it is readonly. == Fixtures This sections explains how to add dummy content to a development application. === Proposals example . In decidim-proposals open `lib/decidim/proposals/component.rb`. . Find the `+component.seeds do...+` block. . Create your dummy content as if you were in a `db/seed.rb` script. === Tips and Tricks * Take advantage of the Faker gem, already in decidim. * If you need content for i18n fields, you can use https://github.com/decidim/decidim/blob/develop/decidim-core/lib/decidim/faker/localized.rb[Localizaed], which uses `Faker` internally. == Assets In order to attach the new component assets to Webpacker configuration, you need to follow a few steps. We are considering two scenarios: - while the component is being developed, where changes in Webpacker configuration should be done in the development for simplicity - once the component has been completed, change the Webpacker templates for the app generators In both cases, changes are the same: 1. Add the new component `app/packs` folder to webpacker.yml 2. Add the new entrypoints of the component to `config/webpack/custom.js` === Updating the development application While the component is being developed, it will be simpler and faster to update Webpacker configuration inside the development app. The new component `app/packs` folder needs to be added to the list of paths that Webpack will use to look for assets. Also, components might have one or many entrypoints (for CSSs and javascripts) and images. These entrypoints need to be manually added to `config/webpack/custom.js`. **Any change in both files** requires to restart `webpack-dev-server` process. Take into account that generating a new development application **overwrites** Webpacker configuration so these changes might be overwriten. That is why it is necessary, once the changes are stable enough, to update the generators files. === Updating Webpacker configuration for the generators Decidim webpacker configuration lives in `decidim-core/lib/decidim/webpacker`. Any change performed in the development app should be replicated in these files. Also, any npm package added to package.json should be replicated in Decidim package.json file. === Updating the design app These changes should also be translated to the design app `config/webpacker.yml` and `config/webpack/custom.js` files. And if there are changes in the npm packages, these should be moved to.