mindpin / slider

移动课程/演示文稿编辑发布服务,slider 是暂定名
0 stars 0 forks source link

复合收藏夹组件的gem 封装 #6

Open fushang318 opened 9 years ago

fushang318 commented 9 years ago

以前的内容

https://github.com/mindpin/web-url-collector/issues/13 https://github.com/destinyd/buckets_demo https://buckets-demo-fushang318.herokuapp.com/

http://oss.aliyuncs.com/pie-documents/20150303/dribbble-buckets-usage.mp4 这个视频演示了一个把资源收藏到收藏夹(bucket)的整个逻辑和UI交互

现在希望把这个逻辑和UI交互整个封装到gem 方便使用 封装的内容包括 1 后台的逻辑 2 前端的交互


设想的使用方法

增加配置信息

config/initialize/simple_bucket.rb

SimpleBucket.config class: Folder

增加模型

config/models/folder.rb

class Folder
  include Mongoid::Document
  include Mongoid::Timestamps
  include SimpleBucket::BucketMethods
end

config/models/book.rb

class Book
  include Mongoid::Document
  include Mongoid::Timestamps
  include SimpleBucket::ResourceMethods
end

给页面元素增加点击弹出 bucket 编辑框

当工程引入bucket组件后,在页面声明带有如下属性的元素 就可以给元素注册点击事件,点击弹出 bucket 编辑框

<!-- 可以是按钮元素,或者其他可点击元素 -->
<button data-rel="simple-bucket" data-res-class="Book" data-res-id="556bd7836465620f70000000">收藏</button>

如何实现的一些思路记录

组件应该需要封装的 controller

logic
  创建 bucket
params
  name
  desc
logic
  给 res 设置 bucket
params
  res_class
  res_id
  bucket_ids

组件应该封装的给页面元素增加点击弹出 bucket 编辑框

组件中有一段js 遍历页面中的 jQuery("*[data-rel='simple-bucket']") 并增加相关逻辑

<!-- 可以是按钮元素,或者其他可点击元素 -->
<button data-rel="simple-bucket" data-res-class="Book" data-res-id="556bd7836465620f70000000">收藏</button>

疑惑

弹出组件的UI交互中没有删除bucket么

destinyd commented 9 years ago

弹出组件的UI交互中没有删除bucket么? 的确没有。dribbble.com是在用户buckets管理页面里面操作的 image

destinyd commented 9 years ago

引用方式参考 enumerize

独立收藏夹情况

class Folder
  extend Bucketerize
  bucketerize resources: ["Book"]
end
class Folder
  extend Bucketerize
  bucketerize bucket: "Folder"
end

共享收藏夹情况

class Folder
  extend Bucketerize
  bucketerize resources: ["Book", "Photo"]
end
class Book
  extend Bucketerize
  bucketerize bucket: "Folder"
end
class Photo
  extend Bucketerize
  bucketerize bucket: "Folder"
end
ben7th commented 9 years ago

个人的一些设计思路供参考:

!!! 经过讨论,设计应该简化,参考下面的楼 !!!

声明方式

独立收藏夹情况

class Shelf
  act_as_bucket :collect => [:book]
  # 赋予 Shelf 收藏夹特性,并且指定被收藏的对象为 Book
  # 一种收藏夹能收藏多种资源,所以这里传数组
end

class Book
  act_as_bucket_resource :into => :shelf
  # 赋予 Book 被收藏资源特性,并且指定它可以被 Shelf 收藏
  # 一种资源只能被一种收藏夹收藏,所以这里传单值
end

class Album
  act_as_bucket :collect => [:photo]
  # 赋予 Album 收藏夹特性,并且指定被收藏的对象为 Photo
end

class Photo
  act_as_bucket_resource :into => :album
  # 赋予 Photo 被收藏资源特性,并且指定它可以被 Album 收藏
end

共用收藏夹情况

class Folder
  act_as_bucket :collect => [:book, :photo]
  # 赋予 Shelf 收藏夹特性,并且指定被收藏的对象为 Book 和 Photo
end

class Book
  act_as_bucket_resource :into => :folder
  # 赋予 Book 被收藏资源特性,并且指定它可以被 Folder 收藏
  # 一种资源只能被一种收藏夹收藏,所以这里传单值
end

class Photo
  act_as_bucket_resource :into => :folder
  # 赋予 Photo 被收藏资源特性,并且指定它可以被 Folder 收藏
end

数据写入方式

添加收藏

# 假设 Folder 是带有 Bucket 特征的类
# 假设 Book 和 Photo 是带有 BucketResource 特征的类
# Folder 允许收集 Book 和 Photo
# 则有:

folder.add_resource book
folder.add_resource photo
# 添加一个资源,通过 Bucket 特征类调用

folder.add_resources [book1, photo1, book2, photo2]
# 添加多个资源,通过 Bucket 特征类调用

book.add_to_bucket folder
photo.add_to_bucket folder
# 添加到一个收藏夹,通过 BucketResource 特征类调用

book.add_to_buckets [folder1, folder2]
photo.add_to_buckets [folder1, folder2]
# 添加到多个收藏夹,通过 BucketResource 特征类调用

移除收藏

folder.remove_resource book
folder.remove_resource photo
folder.remove_resources [book1, book2, photo1, photo2]
# 从收藏夹中移除指定的资源,如果资源本来就不在收藏夹里,则忽略

book.remove_from_bucket folder
photo.remove_from_bucket folder
book.remove_from_buckets [folder1, folder2]
# 将资源从指定收藏夹移除,如果资源本来就不在收藏夹里,则忽略

数据读取方式

folder.resources
#=> 返回包含所有 book photo 的资源集合。通过参数支持分页或通过 scope 支持分页。(查询并列在一起很容易,实现分页支持可能相对比较麻烦)

folder.resources :of => [:book, :photo]
folder.resources :of => [:photo]
folder.resources :of => [:book]
#=> 返回包含指定类型资源的集合。通过参数支持分页或通过 scope 支持分页。(查询并列在一起很容易,实现分页支持可能相对比较麻烦)

# 如果多种资源混合查找确实比较难做到,那就优先支持单一资源查找。
# 重点是需要支持分页。
ben7th commented 9 years ago

进一步简化后的思路

经过讨论,觉得共享收藏夹,以及混合类型查询这些事情都可以通过多态解决 例如,img4ye 0.3 里面封装的对象就是多态 同时代表 图片,视频,音频


个人的一些设计思路供参考:

声明方式

声明一对收藏夹和资源类型

class Album
  act_as_bucket :collect => :photo
  # 赋予 Album 收藏夹特性,并且指定被收藏的对象为 Photo
end

class Photo
  act_as_bucket_resource :into => :album
  # 赋予 Photo 被收藏资源特性,并且指定它可以被 Album 收藏
end

声明多对收藏夹和资源类型

可以指定多组成对出现的收藏夹和被收藏资源。

class Album
  act_as_bucket :collect => :photo
  # 赋予 Album 收藏夹特性,并且指定被收藏的对象为 Photo
end

class Photo
  act_as_bucket_resource :into => :album
  # 赋予 Photo 被收藏资源特性,并且指定它可以被 Album 收藏
end

class Shelf
  act_as_bucket :collect => :book
  # 赋予 Shelf 收藏夹特性,并且指定被收藏的对象为 Book
end

class Book
  act_as_bucket_resource :into => :resource
  # 赋予 Book 被收藏资源特性,并且指定它可以被 Shelf 收藏
end

共用收藏夹情况

class Folder
  act_as_bucket :collect => [:book, :photo]
  # 赋予 Shelf 收藏夹特性,并且指定被收藏的对象为 Book 和 Photo
end

class Book
  act_as_bucket_resource :into => :folder
  # 赋予 Book 被收藏资源特性,并且指定它可以被 Folder 收藏
  # 一种资源只能被一种收藏夹收藏,所以这里传单值
end

class Photo
  act_as_bucket_resource :into => :folder
  # 赋予 Photo 被收藏资源特性,并且指定它可以被 Folder 收藏
end

如何实现一种收藏夹收集多种不同类型的资源?

可以把资源类写成一个多态的类,同时代表多个类型来实现这种逻辑。 本插件不对此提供特别支持。


数据写入方式

添加收藏

# 假设 Folder 是带有 Bucket 特征的类
# 假设 Book 特征的类
# Folder 允许收集 Book
# 则有:

folder.add_resource book
# 添加一个资源,通过 Bucket 特征类调用

folder.add_resources [book1, book2]
# 添加多个资源,通过 Bucket 特征类调用

book.add_to_bucket folder
# 添加到一个收藏夹,通过 BucketResource 特征类调用

book.add_to_buckets [folder1, folder2]
# 添加到多个收藏夹,通过 BucketResource 特征类调用

移除收藏

folder.remove_resource book
folder.remove_resources [book1, book2]
# 从收藏夹中移除指定的资源,如果资源本来就不在收藏夹里,则忽略

book.remove_from_bucket folder
book.remove_from_buckets [folder1, folder2]
# 将资源从指定收藏夹移除,如果资源本来就不在收藏夹里,则忽略

数据读取方式

folder.resources
#=> 返回资源集合(这里返回的应该是 book 查询结果)。通过 scope 支持分页。
destinyd commented 9 years ago

后端api设计

基础API设计

获取buckets列表

path
  /buckets
method
  "GET"
params
  *type String   (例如bucket)
respond
  { type: "bucket", result:[{id: 1, name: 'name1', desc: 'desc1'}, {id: 2, name: 'name2', desc: 'desc2'}] }

获取buckets列表(2015-06-16修改)

path
  /buckets
method
  "GET"
params
  *bucket_type String   (例如bucket)
  resource_type String   (例如resource, 必须与resource_id同时提交, 用于获取是否已经加入收藏夹)
  resource_id String   (例如1, 必须与resource_type同时提交, 用于获取是否已经加入收藏夹)
respond
  { type: "bucket", result:[{id: 1, name: 'name1', desc: 'desc1', added: false}, {id: 2, name: 'name2', desc: 'desc2', added: true }] }

创建bucket

path
  /buckets
method
  "POST"
params
  *type String    (例如bucket)
  *name String  (例如bucket)
  desc  String    (非必填)
respond
  { type: "bucket", result: {id: 1, name: 'name1', desc: 'desc1'}}

添加resource到bucket

path
  /bucketing
method
  "POST"
params
  *bucket_type String    (例如bucket)
  *bucket_id String        (例如"123")
  *resource_type  String    (例如photo)
  *resource_id  String        (例如”234“)
respond
  {result: "ok"}

从bucket移除resource

path
  /bucketing
method
  "DELETE"
params
  *bucket_type String    (例如bucket)
  *bucket_id String        (例如"123")
  *resource_type  String    (例如photo)
  *resource_id  String        (例如”234“)
respond
  {result: "ok"}

附加

参考RailsAdmin,设计可以自定义路径、前缀

  mount RailsAdmin::Engine => '/admin', :as => 'rails_admin'
destinyd commented 9 years ago

新追加一个逻辑,就是限制为当前用户的buckets

需在models与controllers里面,加上相应逻辑

destinyd commented 9 years ago

前端

我们需要在一个dom里面申明一些data内容 ,然后通过JS传入HOOK,处理相应的UI操作,在此,我只简单说明,具体请参考demo https://github.com/destinyd/mindpin_buckets_demo (暂时一页只支持一个Resource)

<a class="add_to_folder btn btn-default" data-rel="mindpin_buckets" data-resource-id="557f9857636865734e000002" data-resource-type="photo" href="javascript:;">添加到Folder</a>

data-rel="mindpin_buckets"为默认激活选择器名称 data-resource-type="photo"为资源类型 data-resource-id="557f9857636865734e000002"为资源类型

class FolderHook extends MindpinHook
  constructor: (@$fm) ->
    @modal_folders = jQuery('#modal-folders')
    @_init()

  _init: () ->
    # 创建按钮点击
    @modal_folders.find('.new').click () => 
      console.log 'click new'
      @modal_new_folder.modal('show')

    @modal_new_folder = jQuery('#modal-new-folder')

    @modal_new_folder.find('.create').click () =>
      console.log 'create'
      name = @modal_new_folder.find('.name').val()
      desc = @modal_new_folder.find('.desc').val()
      console.log name
      console.log desc
      @$fm.create_bucket(name, desc)

  el_click: (el) ->
    console.log 'hook el click'
    # todo
    @modal_folders.modal('show')

  buckets_success: (buckets) =>
    console.log 'hook buckets success'
    console.log buckets
    str = ''
    that = this
    jQuery.each buckets, ->
      bucket = this
      str += that._str_bucket(bucket.id, bucket.name, bucket.added)
    @modal_folders.find('.buckets').html(str)
    # li点击事件绑定:添加、移除到folder
    @modal_folders.on 'click', '.buckets li.group', ->
      console.log 'click li'
      if $(this).hasClass('unbucketed')
        bucket_id = $(this).data('id')
        that.$fm.add_to_bucket(bucket_id) # that.$fm.resource_type, that.$fm.resource_id, bucket_type, bucket_id)
      else
        bucket_id = $(this).data('id')
        bucket_type = that.$fm.bucket_type
        that.$fm.remove_from_bucket(bucket_id)

  create_bucket_success: (bucket) =>
    console.log 'hook create bucket success'
    that = this
    str = @_str_bucket(bucket.id, bucket.name, false)
    @modal_folders.find('.buckets').append str

    @modal_new_folder.find('.name').val('')
    @modal_new_folder.find('.desc').val('')
    @modal_new_folder.modal('hide')

  add_to_bucket_success: (bucket) =>
    @modal_folders.find("[data-id=#{bucket.id}]").removeClass('unbucketed').addClass('bucketed')

  remove_from_bucket_success: (bucket) ->
    console.log 'remove_from_bucket_success'
    console.log bucket
    @modal_folders.find("[data-id=#{bucket.id}]").removeClass('bucketed').addClass('unbucketed')

  _str_bucket: (id, name, added) ->
    "<li class=\"group #{if !added then "un" else ""}bucketed\" data-id=\"#{id}\" id=\"bucket_#{id}\"><a href=\"javascript:;\"><strong>#{name}</strong><!--<span class=\"bucket-meta\">1 photos</span>--><span class=\"bucket-meta\">更新时间</span></a></li>"

jQuery(document).on 'ready page:load', ->
  configs = 
    bucket_type: "Folder"
    hook_class: FolderHook

  window.mindpin_buckets = new MindpinBuckets(configs)

coffee文件主要作用为自定义Hook,改写原有方法,在对应时刻做相应的UI操作(通过configs传入)。

示例为使用Bootstrap的modal来实现demo,你会发现里面使用了一些未写出的dom,都可以在https://github.com/destinyd/mindpin_buckets_demo代码中找到

ben7th commented 9 years ago

对目前前端用法的总结: 针对:https://github.com/destinyd/mindpin_buckets_demo/commit/65e3f5d0f32a4a9bf18f31c8989e2be788e747fe


HOOK dom 定义 定义一个 dom 为 HOOK,代码如下:

<a class="add_to_folder btn btn-default" data-rel="mindpin_buckets" data-resource-id="557f9857636865734e000002" data-resource-type="photo" href="javascript:;">添加到Folder</a>

data-rel="mindpin_buckets" 固定不变,为默认激活选择器名称 data-resource-type="photo" 为资源类型 data-resource-id="557f9857636865734e000002" 为资源ID

这样,页面加载的时候,组件会自动找到上面这个 dom,并把 resource_id 和 resource_type 读入组件来构造实例。


HOOK 类定义 定义一个类,继承 MindpinHook. 在该类的构造函数中,可以自行实现一些事件。 例如,初始化对话框,并且为对话框的按钮注册点击事件。

class FolderHook extends MindpinHook
  constructor: (@api)->
    # do sth..

框架会将一个 MindpinBuckets 的实例传给该 HOOK 类。该实例可以用于调用 API 方法:

@api.create_bucket(name, desc)
# 根据传入的 name, desc 创建一个 bucket

@api.add_to_bucket(bucket_id)
# 将当前资源(根据 dom 读入的资源)添加到指定 ID 的 bucket 中

@api.remove_from_bucket(bucket_id)
# 将当前资源(根据 dom 读入的资源)从指定 ID 的 bucket 中移除

页面初始化

页面初始化时,需要构造一个 MindpinBuckets 实例,并且把 HOOK 类传进去。

jQuery(document).on 'ready page:load', ->
  configs = 
    bucket_type: "Folder"
    hook_class: FolderHook

  window.mindpin_buckets = new MindpinBuckets(configs)

MindpinBuckets 的实例被构造时,会发起读取 buckets 列表的请求。并且把读取到的信息传给 HOOK 类的 buckets_success 方法。 开发者需要实现此方法,增加自己的处理逻辑,例如生成收藏夹列表。


使用过程

页面加载完毕后,点击 HOOK dom 时,会触发 HOOK 类的 el.click() 方法。 开发者需要实现此方法,增加自己的处理逻辑。例如弹出收藏夹对话框。

  el_click: (el) ->
    @modal_folders.modal('show')

在 HOOK 类的构造函数里,开发者需要自行绑定事件,实现对 @api.create_bucket(name, desc) 的调用。 该调用请求完毕后,会触发HOOK 类的 create_bucket_success(bucket) 方法。并把 bukcet 信息传给该方法。 开发者可以自行实现该方法,做一些创建收藏夹的后续处理。例如更新列表。

在 HOOK 类的构造函数里,开发者需要自行绑定事件,实现对 @api.add_to_bucket(name, desc) 的调用。 该调用请求完毕后,会触发HOOK 类的 add_to_bucket_success(bucket) 方法。并把 bukcet 信息传给该方法。 开发者可以自行实现该方法,做一些添加资源到收藏夹的后续处理。例如更新页面信息。

在 HOOK 类的构造函数里,开发者需要自行绑定事件,实现对 @api.remove_from_bucket(name, desc) 的调用。 该调用请求完毕后,会触发HOOK 类的 remove_from_bucket_success(bucket) 方法。并把 bukcet 信息传给该方法。 开发者可以自行实现该方法,做一些添加资源到收藏夹的后续处理。例如更新列表。


使用以上的方法,开发者就可以实现自己的复合收藏夹前端。