# Component

By using the Victor::Component base class, you can compose complex SVG images by having one component call and reference other components.

# Target Image

In the example below, we are creating this output, using Cell, Piece and Board components.

# Usage Pattern

  1. Create a class that inherits from Victor::Component
  2. Implement #width and #height, either as public methods or instance variables.
  3. Implement #body, and use it to add SVG elements and/or embed other components.
  4. Optionally, implement #style, and use it to return a CSS hash, which will be merged to any hosting component.
  5. Optionally, if you want to provide host components with the ability to control x and y, provide them as public methods or instance variables.

# Notes

  1. Components are always generated with 100% width and height, and with a viewBox that is determined by your x, y, width, height properties (x and y default to 0).
  2. Once a component was rendered (#render, #to_s) or saved (#save), the #body method will be called once and once only. This means that at this point the SVG can no longer be altered.
  3. Each component is also a standalone SVG, that can be saved or rendered independently.

# Code

require 'victor'

class Cell < Victor::Component
  attr_reader :dark

  def initialize(x: 0, y: 0, dark: false)
    @dark = dark

    # These 4 do not need attr_reader, Victor::Component handles that.
    # While x and y are optional, width and height are required.
    @x = x
    @y = y
    @width = 100
    @height = 100
  end

  # This method defines the SVG body.
  # Use add.* to add any SVG element.
  def body
    add.rect class: css_class, x: x, y: y, width: width, height: height
  end

  # This optional method returns a CSS hash. It will be merged into any host
  # component automatically.
  def style
    {
      '.cell':       { stroke: :white, stroke_width: 1 },
      '.light.cell': { fill: '#d4a76f' },
      '.dark.cell':  { fill: '#6a3f2b' }
    }
  end

private

  def css_class
    @css_class ||= (dark ? 'dark cell' : 'light cell')
  end
end
require 'victor'

class Piece < Victor::Component
  attr_reader :dark

  def initialize(x: 0, y: 0, dark: false)
    @dark = dark
    @x = x
    @y = y
  end

  # Width and height can bn provided as methods, or as instance variables in
  # the #initialize method.
  def width = @width ||= 80
  def height = @height ||= 80

  # This method defines the SVG body.
  # Use add.* to add any SVG element.
  def body
    add.circle class: css_class,
      cx: x + width / 2, cy: y + height / 2,
      r: [width, height].min / 2
  end

  # This optional method returns a CSS hash. It will be merged into any host
  # component automatically.
  def style
    {
      '.piece': {
        filter: 'drop-shadow(8px 8px 8px rgba(0, 0, 0, 0.5))',
        stoke: :none
      },
      '.light.piece': {
        fill: '#f5f5f5',
      },
      '.dark.piece':  {
        fill: '#333',
      }
    }
  end

private

  def css_class
    @css_class ||= (dark ? 'dark piece' : 'light piece')
  end
end
require 'victor'

class Board < Victor::Component

  # Since all components provide Width and height, we can use these properties
  # to calculate this component's dimensions dynamically.
  def width = @width ||= (cell_width * 8) + (board_margin * 2)
  def height = @height ||= (cell_height * 8) + (board_margin * 2)

  # Separate the SVG generation to logical units.
  def body
    add_frame
    add_cells
    add_pieces
  end

  # Provide a public interface for consumers to add pieces after creating
  # the board instance. Note that once the component was rendered, its SVG
  # is frozen - meaning this method must be called before rendering.
  # For this reason, we only remember the added pieces in an array for later
  # use.
  def add_piece(row, col, dark: false)
    piece = Piece.new dark: dark,
      x: col * cell_width + board_margin + 10,
      y: row * cell_height + board_margin + 10

    pieces.push piece
  end

  def style
    {
      '.board-frame': {
        fill: '#d1bfa7',
        stroke: '#6a3f2b'
      },
      '.inner-frame': {
        fill: '#6a3f2b',
        stroke: '#6a3f2b',
        stroke_width: (board_margin * 0.7).round
      },
    }
  end

private

  def board_margin = @board_margin ||= 50
  def cell_width = @cell_width ||= sample_cell.width
  def cell_height = @cell_height ||= sample_cell.height
  def sample_cell = @sample_cell ||= Cell.new

  def pieces = @pieces ||= []

  # Add a double-rectangle board frame
  def add_frame
    add.rect class: 'board-frame', x: 0, y: 0, width: width, height: height
    add.rect class: 'inner-frame', x: board_margin, y: board_margin,
      width: width - (board_margin * 2),
      height: height - (board_margin * 2)
  end

  # Create the 8x8 grid.
  def add_cells
    8.times do |row|
      dark = row.even?
      8.times do |col|
        cell = Cell.new dark: dark,
          x: col * cell_width + board_margin,
          y: row * cell_height + board_margin

        embed cell
        dark = !dark
      end
    end
  end

  # Add all pieces as requested by #add_piece
  def add_pieces
    pieces.each { |piece| embed piece }
  end
end

See this example on GitHub

# See Also