Skip to content

Commit 06055b5

Browse files
authored
Merge pull request #8 from blocknotes/feature/js-refactoring-3
JS refactoring (3)
2 parents 0b287bd + 27b5580 commit 06055b5

File tree

4 files changed

+104
-63
lines changed

4 files changed

+104
-63
lines changed

README.md

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ The easiest way to show how this plugin works is looking the examples [below](#e
2020
```
2121

2222
## Options
23-
Options are passed to fields using *input_html* parameter as *data* attributes:
23+
Options are passed to fields using *input_html* parameter as *data* attributes.
24+
25+
Conditions:
2426
- **data-if**: check a condition, values:
2527
+ **checked**: check if a checkbox is checked
2628
+ **not_checked**: check if a checkbox is not checked
@@ -29,17 +31,22 @@ Options are passed to fields using *input_html* parameter as *data* attributes:
2931
+ **changed**: check if the value of an input is changed (dirty)
3032
- **data-eq**: check if a field has a specific value
3133
- **data-not**: check if a field hasn't a specific value
32-
- **data-target**: target css selector (from parent fieldset, look for the closest match)
33-
- **data-gtarget**: target css selector globally
34-
- **data-then**: the action to trigger (alias **data-action**), values:
34+
- **data-function**: check the return value of a custom function
35+
36+
Actions:
37+
- **data-then**: action to trigger (alias **data-action**), values:
3538
+ **hide**: hides elements
3639
+ **slide**: hides elements (using sliding)
3740
+ **fade**: hides elements (using fading)
3841
+ **addClass**: adds classes
3942
+ **setValue**: set a value
40-
+ **callback**: call a function
41-
- **data-function**: check the return value of a custom function
42-
- **data-arg**: argument passed to the custom set function (as array of strings)
43+
+ **callback**: call a function (with arguments: **data-args**)
44+
- **data-else**: action to trigger when the condition check is not true
45+
- **data-args**: arguments passed to the callback function
46+
47+
Targets:
48+
- **data-target**: target css selector (from parent fieldset, look for the closest match)
49+
- **data-gtarget**: target css selector globally
4350

4451
A check condition or a custom check function are required. A trigger action is required too, unless you are using a custom function (in that case it is optional).
4552

@@ -59,17 +66,25 @@ form do |f|
5966
end
6067
```
6168

62-
- Add 3 classes (*first*, *second*, *third*) if a checkbox is not checked:
69+
- Add 3 classes (*first*, *second*, *third*) if a checkbox is not checked, else add "forth" class:
6370

64-
`f.input :published, input_html: { data: { if: 'not_checked', then: 'addClass first second third', target: '.grp1' } }`
71+
```rb
72+
data = { if: 'not_checked', then: 'addClass first second third', target: '.grp1', else: 'addClass forth' }
73+
f.input :published, input_html: { data: data }
74+
```
6575

6676
- Set another field value if a string field is blank:
6777

68-
`f.input :title, input_html: { data: { if: 'blank', then: 'setValue 10', target: '#article_position' } }`
78+
```rb
79+
f.input :title, input_html: { data: { if: 'blank', then: 'setValue 10', target: '#article_position' } }
80+
```
6981

7082
- Use a custom function for conditional check (*title_not_empty()* must be available on global scope) (with alternative syntax for data attributes):
7183

72-
`f.input :title, input_html: { 'data-function': 'title_empty', 'data-then': 'slide', 'data-target': '#article_description_input' }`
84+
```rb
85+
attrs = { 'data-function': 'title_empty', 'data-then': 'slide', 'data-target': '#article_description_input' }
86+
f.input :title, input_html: attrs
87+
```
7388

7489
```js
7590
function title_empty(el) {
@@ -79,7 +94,10 @@ function title_empty(el) {
7994

8095
- Call a callback function as action:
8196

82-
`f.input :published, input_html: { data: { if: 'checked', then: 'callback set_title', args: '["Unpublished !"]' } }`
97+
```rb
98+
data = { if: 'checked', then: 'callback set_title', args: '["Unpublished !"]' }
99+
f.input :published, input_html: { data: data }
100+
```
83101

84102
```js
85103
function set_title(args) {
@@ -92,7 +110,10 @@ function set_title(args) {
92110

93111
- Custom function without action:
94112

95-
`f2.input :category, as: :select, collection: [ [ 'Cat 1', 'cat1' ], [ 'Cat 2', 'cat2' ], [ 'Cat 3', 'cat3' ] ], input_html: { 'data-function': 'on_change_category' }`
113+
```rb
114+
collection = [['Cat 1', 'cat1'], ['Cat 2', 'cat2'], ['Cat 3', 'cat3']]
115+
f2.input :category, as: :select, collection: collection, input_html: { 'data-function': 'on_change_category' }
116+
```
96117

97118
```js
98119
function on_change_category(el) {

app/assets/javascripts/activeadmin/dynamic_fields.js

Lines changed: 60 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
},
1313
fade: el => el.fadeOut(),
1414
hide: el => el.hide(),
15-
setValue: (el, value) => dfSetValue(el, value),
15+
setValue: (el, value) => {
16+
if (el.attr('type') == 'checkbox') el.prop('checked', value == '1')
17+
else el.val(value)
18+
el.trigger('change')
19+
},
1620
slide: el => el.slideUp()
1721
}
1822

@@ -33,58 +37,65 @@
3337
slide: el => el.slideDown()
3438
}
3539

36-
function dfEvalCondition(el) {
37-
let condition = CONDITIONS[el.data('if')]
38-
let condition_arg
40+
class Field {
41+
constructor(el) {
42+
const action = el.data('then') || el.data('action') || ''
43+
const action_name = action.split(' ', 1)[0]
44+
const else_action = el.data('else') || ''
45+
const else_action_name = else_action.split(' ', 1)[0]
46+
47+
this.el = el
48+
this.action = ACTIONS[action_name]
49+
this.action_arg = action.substring(action.indexOf(' ') + 1)
50+
this.reverse_action = REVERSE_ACTIONS[action_name]
51+
this.else_action = ACTIONS[else_action_name]
52+
this.else_action_arg = else_action.substring(else_action.indexOf(' ') + 1)
53+
this.else_reverse_action = REVERSE_ACTIONS[else_action_name]
54+
this.condition = CONDITIONS[el.data('if')]
55+
if (!this.condition && el.data('eq')) {
56+
[this.condition, this.condition_arg] = [CONDITIONS['eq'], el.data('eq')]
57+
}
58+
if (!this.condition && el.data('not')) {
59+
[this.condition, this.condition_arg] = [CONDITIONS['not'], el.data('not')]
60+
}
61+
this.custom_function = el.data('function')
62+
if (!this.condition && this.custom_function) {
63+
this.condition = window[this.custom_function]
64+
if (!this.condition) {
65+
el.attr('data-df-errors', 'custom function not found')
66+
console.warn(`activeadmin_dynamic_fields custom function not found: ${this.custom_function}`)
67+
}
68+
}
3969

40-
if(!condition && el.data('eq')) {
41-
condition = CONDITIONS['eq']
42-
condition_arg = el.data('eq')
43-
}
44-
if(!condition && el.data('not')) {
45-
condition = CONDITIONS['not']
46-
condition_arg = el.data('not')
70+
// closest find for has many associations
71+
if (el.data('target')) this.target = el.closest('fieldset').find(el.data('target'))
72+
else if (el.data('gtarget')) this.target = $(el.data('gtarget'))
73+
if (action_name == 'callback') this.target = el
4774
}
48-
if(!condition && el.data('function')) {
49-
condition = window[el.data('function')]
50-
if(!condition) {
51-
el.attr('data-df-errors', 'custom function not found')
52-
console.warn(`activeadmin_dynamic_fields custom function not found: ${el.data('function')}`)
75+
76+
apply(el) {
77+
if (this.condition(el, this.condition_arg)) {
78+
if (this.else_reverse_action) this.else_reverse_action(this.target, this.else_action_arg)
79+
this.action(this.target, this.action_arg)
80+
}
81+
else {
82+
if (this.reverse_action) this.reverse_action(this.target, this.action_arg)
83+
if (this.else_action) this.else_action(this.target, this.else_action_arg)
5384
}
5485
}
5586

56-
return [condition, condition_arg]
57-
}
87+
is_valid() {
88+
if (!this.condition) return false
89+
if (!this.action && !this.custom_function) return false
5890

59-
function dfInitField(el) {
60-
const [condition, condition_arg] = dfEvalCondition(el)
61-
const action_name = (el.data('then') || el.data('action') || '').substr(0, 8)
62-
const action = ACTIONS[action_name]
63-
const arg = (el.data('then') || el.data('action') || '').substr(9)
64-
const reverse_action = REVERSE_ACTIONS[action_name]
65-
if (typeof condition === 'undefined') return
66-
if (typeof action === 'undefined' && !el.data('function')) return
67-
68-
// closest find for has many associations
69-
let target
70-
if (el.data('target')) target = el.closest('fieldset').find(el.data('target'))
71-
else if (el.data('gtarget')) target = $(el.data('gtarget'))
72-
if (action_name == 'callback') target = el
73-
74-
if (condition(el, condition_arg) && el.data('if') != 'changed') action(target, arg)
75-
else if (reverse_action) reverse_action(target, arg)
76-
77-
el.on('change', () => {
78-
if (condition(el, condition_arg)) action(target, arg)
79-
else if (reverse_action) reverse_action(target, arg)
80-
})
81-
}
91+
return true
92+
}
8293

83-
// Set the value of an element
84-
function dfSetValue(el, val) {
85-
if (el.attr('type') == 'checkbox') el.prop('checked', val == '1')
86-
else el.val(val)
87-
el.trigger('change')
94+
setup() {
95+
if (!this.is_valid()) return
96+
if (this.el.data('if') != 'changed') this.apply(this.el)
97+
this.el.on('change', () => this.apply(this.el))
98+
}
8899
}
89100

90101
// Inline update - must be called binded on the editing element
@@ -144,14 +155,14 @@
144155
// Setup dynamic fields
145156
const selectors = '.active_admin .input [data-if], .active_admin .input [data-eq], .active_admin .input [data-not], .active_admin .input [data-function]'
146157
$(selectors).each(function () {
147-
dfInitField($(this))
158+
new Field($(this)).setup()
148159
})
149160

150161
// Setup dynamic fields for associations
151162
$('.active_admin .has_many_container').on('has_many_add:after', () => {
152163
$(selectors).each(function () {
153-
dfInitField($(this))
154-
})
164+
new Field($(this)).setup()
165+
})
155166
})
156167

157168
// Set dialog icon link

spec/dummy/app/admin/posts.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ def add_field(form, name, type, data, override_options = {})
143143
df302 = { if: 'checked', then: 'addClass red', target: 'body.active_admin' }
144144
add_field(f, :data_field_302, :boolean, df302)
145145

146-
# ---
146+
# --- else
147+
df321 = { if: 'checked', then: 'addClass red', target: '#post_data_field_321_input label', else: 'addClass green' }
148+
add_field(f, :data_field_321, :boolean, df321)
147149
end
148150

149151
f.inputs 'Tags' do

spec/system/dynamic_fields_spec.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@
143143

144144
find('#post_data_field_302').click # checks that using simply "target" will not work
145145
expect(page).not_to have_css('body.active_admin.red')
146+
147+
# --- else
148+
expect(page).not_to have_css('#post_data_field_321_input label.red')
149+
expect(page).to have_css('#post_data_field_321_input label.green')
150+
find('#post_data_field_321').click
151+
expect(page).to have_css('#post_data_field_321_input label.red')
152+
expect(page).not_to have_css('#post_data_field_321_input label.green')
146153
end
147154
end
148155

0 commit comments

Comments
 (0)