Ruby on Rails 实现的 PropertyGrid






4.79/5 (4投票s)
使用 JQuery UI 和最少的 Javascript 创建一个动态属性网格编辑器,可以以流畅的编程风格或最小的 DSL 进行初始化。
从 GitHub 获取源代码
git clone https://github.com/cliftonm/property_grid_demo
此控件的代码现在可以作为 gem 安装
gem install property_grid
并可从此处下载
git clone https://github.com/cliftonm/property_grid
引言
我需要一个通用的属性网格编辑器,它支持一些高级功能,如日期/时间选择器、颜色选择器等,基于仅在运行时才知道的记录字段(这最终是我“Spider UI”文章系列下一部分的内容)。 这里有一个时髦的基于 Javascript 的属性网格 这里,但我想要一些 Javascript 最少,并且更“Ruby-on-Rails”的东西。 我还想要一个可以与记录字段类型很好地集成的服务器端控件,它会根据表字段等模式信息动态生成网格。
我构建了一组类来方便在服务器端构建属性网格控件的内容。 您会注意到我选择了实际的类和“流畅”的编程风格,但如果您不喜欢实际实现使用“流畅”技术的方式,我还构建了一个非常小的内部领域特定语言 (DSL),您可以使用它代替——基本上只是隐藏(使用静态数据)构建属性网格实例的内部管理的调用方法。
与我之前的文章一样,我将使用 Sass 和 Slim 脚本来编写 CSS 和 HTML 标记。
支持类
有几个支持类
- PropertyGrid - 组和组属性的容器
- Group - 属性组
- GroupProperty - 组内的属性
类 PropertyGrid
# A PropertyGrid container # A property grid consists of property groups. class PropertyGrid attr_accessor :groups def initialize @groups = [] end # Give a group name, creates a group. def add_group(name) group = Group.new group.name = name @groups << group yield(group) # yields to block creating group properties self # returns the PropertyGrid instance end end
此类有两个要点
- 因为
add_group
执行yield(group)
,所以调用者可以提供一个块来添加组属性。 - 因为
add_group
返回self
,所以调用者可以继续以流畅的编程风格添加更多组。
因此,我们可以编写这样的代码
@property_grid = PropertyGrid.new(). add_group('Text Input') do |group| # add group properties here. end. #<---- note this syntax add_group('Date and Time Pickers') do |group| # add group properties here. end
注意“点”:end.
- 因为 add_group
在 yield 之后返回 self
,所以我们可以使用流畅的编程风格继续添加组。
类 Group
# Defines a PropertyGrid group # A group has a name and a collection of properties. class PropertyGroup attr_accessor :name attr_accessor :properties def initialize @name = nil @properties = [] end def add_property(var, name, property_type = :string, collection = nil) group_property = GroupProperty.new(var, name, property_type, collection) @properties << group_property self end end
一个组有一个名称并管理一个属性集合。 add_property
类返回 self
,所以我们再次可以使用流畅的表示法
group.add_property(:prop_c, 'Date', :date). add_property(:prop_d, 'Time', :time). add_property(:prop_e, 'Date/Time', :datetime)
注意每次调用 add_property
后的“点”,允许我们再次调用 add_property
,操作同一个组实例。
这并不能阻止我们使用更符合 Ruby 习惯的语法,例如
group.properties << GroupProperty.new(:prop_c, 'Date', :date) << GroupProperty.new(:prop_d, 'Time', :time) << GroupProperty.new(:prop_e, "Date/Time", :datetime)
类 GroupProperty
此类是实际属性的容器
include PropertyGridHelpers class GroupProperty attr_accessor :property_var attr_accessor :property_name attr_accessor :property_type attr_accessor :property_collection # some of these use jquery: https://jqueryui.jqueryjs.cn/ def initialize(var, name, property_type, collection = nil) @property_var = var @property_name = name @property_type = property_type @property_collection = collection end def get_input_control form_type = get_property_type_map[@property_type] raise "Property '#{@property_type}' is not mapped to an input control" if form_type.nil? erb = get_erb(form_type) erb end end
我稍后会讨论 get_erb
的作用。
请注意,需要三个字段
- 模型属性的符号名称
- 属性的显示文本
- 属性类型
可选地,可以传入一个集合,它支持下拉控件。 集合可以是简单的数组
['Apples', 'Oranges', 'Pears']
或“记录”,实现 id
和 name
属性,例如
# A demo of using id and name in a combo box class ARecord attr_accessor :id attr_accessor :name def initialize(id, name) @id = id; @name = name end end @records = [ ARecord.new(1, 'California'), ARecord.new(2, 'New York'), ARecord.new(3, 'Rhode Island'), ]
这适用于 ActiveRecord 对象集合。
类 ControlType
此类是渲染 Web 控件所需信息的容器
class ControlType attr_accessor :type_name attr_accessor :class_name def initialize(type_name, class_name = nil) @type_name = type_name @class_name = class_name end end
这非常基础——它只是类型名称和一个可选的类名。 目前,类名仅用于 jQuery 控件。
定义属性类型
属性类型定义在 property_grid_helpers.rb 中——这是一个简单的函数,返回一个 type => ControlType
的哈希数组。
def get_property_type_map { string: ControlType.new('text_field'), text: ControlType.new('text_area'), boolean: ControlType.new('check_box'), password: ControlType.new('password_field'), date: ControlType.new('datepicker'), datetime: ControlType.new('text_field', 'jq_dateTimePicker'), time: ControlType.new('text_field', 'jq_timePicker'), color: ControlType.new('text_field', 'jq_colorPicker'), list: ControlType.new('select'), db_list: ControlType.new('select') } end
您可以在此处扩展或更改类型映射到 Web 查询的规范。 显然,您不限于使用 jQuery 控件。
DSL 实现会是什么样子?
让我们看看如果我将属性网格容器对象写成 DSL 会是什么样子。 如果您有兴趣,这里有一篇关于在 Ruby 中编写内部 DSL 的精彩教程 这里,我正在做的事情应该看起来非常相似。 基本上,DSL 使用 Builder 模式,如果您对 Ruby 中的设计模式感兴趣,这里是一个很好的教程。
我们想要的是能够声明一个属性网格实例,就好像它是 Ruby 语言的一部分一样。 所以我将从
@property_grid = new_property_grid group 'Text Input' group_property 'Text', :prop_a group_property 'Password', :prop_b, :password group 'Date and Time Pickers' group_property 'Date', :prop_c, :date group_property 'Time', :prop_d, :date group_property 'Date/Time', :prop_e, :datetime group 'State' group_property 'Boolean', :prop_f, :boolean group 'Miscellaneous' group_property 'Color', :prop_g, :color group 'Lists' group_property 'Basic List', :prop_h, :list, ['Apples', 'Oranges', 'Pears'] group_property 'ID - Name List', :prop_i, :db_list, @records
实现包含三个方法
- new_property_grid
- group
- 属性
它们基本上是用于构建属性组及其属性实例的工厂模式。 实现位于一个模块中,并利用我们底层的类
module PropertyGridDsl def new_property_grid(name = nil) @__property_grid = PropertyGrid.new @__property_grid end def group(name) group = Group.new group.name = name @__property_grid.groups << group group end def group_property(name, var, type = :string, collection = nil) group_property = GroupProperty.new(var, name, type, collection) @__property_grid.groups.last.properties << group_property group_property end end
此实现利用了 @__property_grid
变量,该变量维护当前在 DSL 脚本中应用的实例。 我们不使用单例模式,因为我们希望允许在同一网页上拥有多个属性网格实例。
正如 Martin Fowler 在 这里所写,虽然内部 DSL 经常会增加“语法噪音”,但一个写得好的 DSL 实际上应该减少“语法噪音”,就像这个简单的 DSL 所做的那样。 例如,比较 DSL
@property_grid = new_property_grid group 'Text Input' group_property 'Text', :prop_a
与非 DSL 实现:
@property_grid = PropertyGrid.new(). add_group('Text Input') do |group| group.add_property(:prop_a, 'Text'). add_property(:prop_b, 'Password', :password) end
当然,使用类实现,即使是“流畅”的形式,也比 DSL 更嘈杂!
整合
您需要一个视图、一个控制器和一个模型来将所有这些组合在一起。
视图
基本视图很简单。 给定模型,我们实例化一个列表控件,其中每个列表项本身都是一个具有两列和一行的表格
=fields_for @property_grid_record do |f| .property_grid ul - @property_grid.groups.each_with_index do |group, index| li.expanded class="expandableGroup#{index}" = group.name .property_group div class="property_group#{index}" table tr th Property th Value - group.properties.each do |prop| tr td = prop.property_name td.last - # must be processed here so that ERB has the context (the 'self') of the HTML pre-processor. = render inline: ERB.new(prop.get_input_control).result(binding) = javascript_tag @javascript javascript: $(".jq_dateTimePicker").datetimepicker({dateFormat: 'mm/dd/yy', timeFormat: 'hh:mm tt'}); $(".jq_timePicker").timepicker({timeFormat: "hh:mm tt"}); $(".jq_colorPicker").minicolors()
我将不展示驱动此结构视觉呈现的 CSS。
Javascript
请注意,有两个 Javascript 部分。 一个是直接编码在表单中以支持 jQuery dateTimePicker
、timePicker
和 colorPicker
控件。
另一个 Javascript 是以编程方式生成的,因为它控制属性组是折叠还是展开,这需要为每个属性组提供唯一的处理程序。 由于这仅在运行时知道,因此 Javascript 由此函数(在 property_grid_helpers.rb 中)生成
def get_javascript_for_group(index) js = %Q| $(".expandableGroup[idx]").click(function() { var hidden = $(".property_group[idx]").is(":hidden"); // get the value BEFORE making the slideToggle call. $(".property_group[idx]").slideToggle('slow'); // At this point, $(".property_group0").is(":hidden"); // ALWAYS RETURNS FALSE if (!hidden) // Remember, this is state that the div WAS in. { $(".expandableGroup[idx]").removeClass('expanded'); $(".expandableGroup[idx]").addClass('collapsed'); } else { $(".expandableGroup[idx]").removeClass('collapsed'); $(".expandableGroup[idx]").addClass('expanded'); } }); |.gsub('[idx]', index.to_s) js end
ERB
请注意上面这一行
= render inline: ERB.new(prop.get_input_control).result(binding)
这会处理 ERB 代码,该代码也是以编程方式生成的,因为我们需要一个特定于属性类型的控件。 这是由我们之前看到的 get_erb
函数生成的。
# Returns the erb for a given form type. This code handles the construction of the web control that will display # the content of a property in the property grid. # The web page must utilize a field_for ... |f| for this construction to work. def get_erb(form_type) erb = "<%= f.#{form_type.type_name} :#{@property_var}" erb << ", class: '#{form_type.class_name}'" if form_type.class_name.present? erb << ", #{@property_collection}" if @property_collection.present? && @property_type == :list erb << ", options_from_collection_for_select(f.object.records, :id, :name, f.object.#{@property_var})" if @property_collection.present? && @property_type == :db_list erb << "%>" erb end
模型
我们需要一个模型来存储我们的属性值。 在演示中,模型位于 property_grid_record.rb
class PropertyGridRecord < NonPersistedActiveRecord attr_accessor :prop_a attr_accessor :prop_b attr_accessor :prop_c attr_accessor :prop_d attr_accessor :prop_e attr_accessor :prop_f attr_accessor :prop_g attr_accessor :prop_h attr_accessor :prop_i attr_accessor :records def initialize @records = [ ARecord.new(1, 'California'), ARecord.new(2, 'New York'), ARecord.new(3, 'Rhode Island'), ] @prop_a = 'Hello World' @prop_b = 'Password!' @prop_c = '08/19/1962' @prop_d = '12:32 pm' @prop_e = '08/19/1962 12:32 pm' @prop_f = true @prop_g = '#ff0000' @prop_h = 'Pears' @prop_i = 2 end end
这只是初始化我们的测试数据。
控制器
控制器将所有内容组合在一起,实例化模型,指定属性网格属性和类型,并获取以编程方式生成的 Javascript
include PropertyGridDsl include PropertyGridHelpers class DemoPageController < ApplicationController def index initialize_attributes end private def initialize_attributes @property_grid_record = PropertyGridRecord.new @property_grid = define_property_grid @javascript = generate_javascript_for_property_groups(@property_grid) end def define_property_grid grid = new_property_grid group 'Text Input' group_property 'Text', :prop_a group_property 'Password', :prop_b, :password group 'Date and Time Pickers' group_property 'Date', :prop_c, :date group_property 'Time', :prop_d, :date group_property 'Date/Time', :prop_e, :datetime group 'State' group_property 'Boolean', :prop_f, :boolean group 'Miscellaneous' group_property 'Color', :prop_g, :color group 'Lists' group_property 'Basic List', :prop_h, :list, ['Apples', 'Oranges', 'Pears'] group_property 'ID - Name List', :prop_i, :db_list, @property_grid_record.records grid end end
还有一个支持函数(在 property_grid_helpers.rb 中)
def generate_javascript_for_property_groups(grid) javascript = '' grid.groups.each_with_index do |grp, index| javascript << get_javascript_for_group(index) end javascript end
瞧
结论
像这样的东西应该很容易移植到 C# / ASP.NET,我很乐意听任何这样做的人的反馈。 否则,请尽情享用,并告诉我您是如何改进这个概念的。