vue-helpers 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ require 'erb'
2
+
3
+ module Vue
4
+ module Helpers
5
+
6
+ module CoreRefinements
7
+ refine String do
8
+ def interpolate(**locals)
9
+ gsub(/\#\{/, '%{') % locals
10
+ end
11
+
12
+ def camelize
13
+ #split(/[_-]/).collect(&:capitalize).join
14
+ split(/\W|_/).collect(&:capitalize).join
15
+ end
16
+
17
+ def kebabize
18
+ split(/\W|_/).collect(&:downcase).join('-')
19
+ end
20
+
21
+ def escape_backticks
22
+ gsub(/\`/,'\\\`')
23
+ end
24
+ end
25
+
26
+ refine Hash do
27
+ def to_html_attributes
28
+ inject(''){|o, kv| o.to_s << %Q(#{kv[0]}="#{kv[1]}")}
29
+ end
30
+ end
31
+
32
+ refine Dir.singleton_class do
33
+ # Args are like Dir.glob() with **opts accaptable.
34
+ #
35
+ # Returns list of files breadth-first.
36
+ # Pass :no_recurse=>true to block directory recursion.
37
+ # Pass a block to yield each found path to the block.
38
+ #
39
+ def breadth_first(pattern, flags=0, base: Dir.getwd, **opts, &block)
40
+ files, dirs = [], []
41
+ Dir.glob(File.join(base, pattern), flags).each{|path| FileTest.directory?(path) ? dirs.push(path) : files.push(path)}
42
+
43
+ files.each{|f| yield(f)} if block_given?
44
+ dirs.each{|dir| files.concat(breadth_first(pattern, flags, base:dir, &block))} unless opts[:no_recurse]
45
+
46
+ files
47
+ end
48
+ end
49
+ end # CoreRefinements
50
+
51
+ module ErbPrepend
52
+ def initialize(*args)
53
+ #puts "ERB.new: #{args.to_yaml}"
54
+ #puts "ERB.new with eoutvar: #{args[3]}"
55
+ args[3] ||= Vue::Helpers.default_outvar
56
+ super
57
+ end
58
+
59
+ def set_eoutvar(*args)
60
+ #puts "ERB.set_eoutvar: #{args[1]}"
61
+ Thread.current.instance_variable_set(:@current_eoutvar, args[1])
62
+ super
63
+ end
64
+ end
65
+ ::ERB.send(:prepend, ErbPrepend)
66
+
67
+ module ControllerPrepend
68
+ # Assign value to undefined @outvar
69
+ # TODO: This might not be needed any more, after implementation of ERB.set_eoutvar hack.
70
+ # def initialize(*args)
71
+ # super
72
+ # unless defined?(@outvar)
73
+ # @outvar ||= Vue::Helpers.default_outvar
74
+ # end
75
+ # end
76
+ end
77
+
78
+ end # Helpers
79
+ end # Vue
@@ -0,0 +1,5 @@
1
+ module Vue
2
+ module Helpers
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,83 @@
1
+ require_relative 'utilities'
2
+ require_relative 'helper_refinements'
3
+ require_relative 'vue_object'
4
+
5
+ module Vue
6
+ module Helpers
7
+ using CoreRefinements
8
+ using HelperRefinements
9
+
10
+ class VueComponent < VueObject
11
+ def type; 'component'; end
12
+
13
+ #puts "VueComponent defining @defaults"
14
+ @defaults = {
15
+ root_name: nil, #Vue::Helpers.root_name,
16
+ register_local: nil,
17
+ template_literal: nil
18
+ }
19
+
20
+ attr_writer *defaults.keys
21
+ custom_attr_reader *defaults.keys
22
+
23
+ # Gets root object
24
+ def root
25
+ repo.root(root_name)
26
+ end
27
+
28
+ # Renders the html block to replace ruby vue_component tags.
29
+ # TODO: Are locals used here? Do they work?
30
+ def render(tag_name=nil, locals:{}, attributes:{}, &block)
31
+ # Adds 'is' attribute to html vue-component element,
32
+ # if the user specifies an alternate 'tag_name' (default tag_name is name-of-component).
33
+ if tag_name
34
+ attributes['is'] = name
35
+ end
36
+
37
+ block_content = context.capture_html(root_name:root_name, locals:locals, &block) if block_given?
38
+
39
+ # TODO: Are locals being passed properly here?
40
+ wrapper(:component_call_html,
41
+ name:name,
42
+ tag_name:tag_name,
43
+ el_name:(tag_name || name).to_s.kebabize,
44
+ block_content:block_content.to_s,
45
+ attributes_string:attributes.to_html_attributes,
46
+ **locals
47
+ )
48
+ end
49
+
50
+ # Builds js output string.
51
+ # TODO: Follow this backwards/upstream to determine if parsed_template, parsed_script, and locals are being handled correctly.
52
+ #def to_component_js(register_local:Vue::Helpers.register_local, template_literal:Vue::Helpers.template_literal, locals:{}) #, **options)
53
+ def to_component_js(locals:{})
54
+ # The above **options are not used yet, but need somewhere to catch extra stuff.
55
+ template_spec = template_literal ? "\`#{parsed_template(locals).to_s.escape_backticks}\`" : "'##{name}-template'"
56
+ js_output = register_local \
57
+ ? 'var #{name} = {template: #{template_spec}, \2;'
58
+ : 'var #{name} = Vue.component("#{name}", {template: #{template_spec}, \2);' # ) << ")"
59
+
60
+ # TODO: Make escaping backticks optional, as they could break user templates with nested backtick blocks, like ${``}.
61
+ _parsed_script = parsed_script(locals)
62
+ _parsed_script.gsub(
63
+ /export\s+default\s*(\{|Vue.component\s*\([^\{]*\{)(.*$)/m,
64
+ js_output
65
+ ).interpolate(name: name.to_s.camelize, template_spec: template_spec) if _parsed_script
66
+ end
67
+
68
+ # TODO: Follow this backwards/upstream to determine if parsed_template, parsed_script, and locals are being handled correctly.
69
+ def get_x_template(**locals)
70
+ wrapper(:x_template_html, name:name, template:parsed_template(locals), **locals)
71
+ end
72
+
73
+ # def template_literal?
74
+ # case
75
+ # when !template_literal.nil?; template_literal
76
+ # when !root.template_literal.nil?; root.template_literal
77
+ # when !Vue::Helpers.template_literal.nil?; Vue::Helpers.template_literal
78
+ # end
79
+ # end
80
+
81
+ end # VueComponent
82
+ end # Helpers
83
+ end # Vue
@@ -0,0 +1,201 @@
1
+ require_relative 'utilities'
2
+ require_relative 'helper_refinements'
3
+
4
+ module Vue
5
+ module Helpers
6
+ using CoreRefinements
7
+ using HelperRefinements
8
+
9
+
10
+ # NOTE: Vue components can be called MULTIPLE times,
11
+ # so we can't store the calling args OR the block here.
12
+ #
13
+ # But note that Vue root-apps can only be called once,
14
+ # so should we continue to store the vue-root calling args & block here,
15
+ # or pass them in at run-time as well? I think it ALL has to be dynamic.
16
+ #
17
+ class VueObject
18
+
19
+ @defaults = {
20
+ # Only what's necessary to load dot-vue file.
21
+ name: nil,
22
+ root_name: nil,
23
+ file_name: nil,
24
+ template_engine: nil,
25
+ #locals: {},
26
+
27
+ # The loaded (but not rendered or parsed) dot-vue file as Tilt teplate.
28
+ # See 'initialize_options()' below
29
+ tilt_template: nil,
30
+
31
+ # Vue-specific options to be inserted in js object.
32
+ # Remember that component data must be a function in the js object.
33
+ data: {},
34
+ watch: {},
35
+ computed: {},
36
+
37
+ # Utility
38
+ repo: nil
39
+ }
40
+
41
+ # Concatenates subclass defaults with master class defaults.
42
+ def self.defaults
43
+ super_defaults = superclass.singleton_class.method_defined?(__method__) ? superclass.defaults : (@defaults || {})
44
+ super_defaults.merge(@defaults || {})
45
+ end
46
+
47
+ # Creates custom attr readers that tie in to defaults.
48
+ def self.custom_attr_reader(*args)
49
+ args.each do |a|
50
+ define_method(a) do |use_default=true|
51
+ get_attribute(a, use_default)
52
+ end
53
+ end
54
+ end
55
+
56
+ #attr_accessor :repo, *defaults.keys
57
+ attr_reader :initialized
58
+
59
+
60
+ ### Internal methods
61
+
62
+ def defaults
63
+ self.class.defaults
64
+ end
65
+
66
+ # For debugging.
67
+ def print_ivars
68
+ puts "\nIVARS:"
69
+ instance_variables.each do |v|
70
+ unless v.to_s[/repo/i]
71
+ puts "#{v}: #{instance_variable_get(v)}"
72
+ end
73
+ end
74
+ end
75
+
76
+ def initialize(name, **options)
77
+ @name = name
78
+ #puts "VueObject created: #{name}, self: #{self}"
79
+ initialize_options(**options)
80
+ end
81
+
82
+ def initialize_options(**options)
83
+ @repo ||= options.delete(:repo)
84
+ #locals = options.delete(:locals) || {}
85
+ #puts "\n#{self.class.name}.initialize_options '#{name}' #{options.inspect}"
86
+ return self unless options.size > 0 && !@initialized
87
+ locals = options.delete(:locals) || {}
88
+ #puts "\n#{self.class.name}.initialize_options '#{name}', #{options.inspect}, locals:#{locals.inspect}"
89
+
90
+ merged_options = defaults.dup.merge(options)
91
+
92
+ # Sets each default ivar, unless ivar is already non-nil.
93
+ merged_options.each do |k,v|
94
+ #puts "Setting ivar '#{k}' with '#{v}', was previously '#{instance_variable_get('@' + k.to_s)}'"
95
+ #instance_variable_set("@#{k}", v) if v && instance_variable_get("@#{k}").nil? #!(v.respond_to?(:empty) && v.empty?)
96
+ instance_variable_set("@#{k}", v) if instance_variable_get("@#{k}").nil?
97
+ end
98
+
99
+ @file_name ||= @name
100
+
101
+ #load_dot_vue if file_name
102
+ load_tilt_template if file_name #&& !tilt_template
103
+
104
+ # We need this to discover subcomponents, otherwise
105
+ # vue_app won't know about them until it's too late.
106
+ render_template(**locals)
107
+
108
+ #puts "\n#{self.class.name} initialized."
109
+ #print_ivars
110
+
111
+ @initialized = true
112
+ #puts "VueObject initialized options: #{name}, self: #{self}"
113
+ self
114
+ end
115
+
116
+ # Loads a dot-vue into a tilt template, but doesn't render or parse it.
117
+ def load_tilt_template
118
+ self.tilt_template = context.load_template(file_name.to_sym, template_engine:template_engine(false))
119
+ end
120
+
121
+ # Renders loaded tilt_template.
122
+ # TODO: Find a way to easily insert attributes like 'name', 'root_name', etc. into locals.
123
+ # Or just insert the current vue object instance into the locals
124
+ def render_template(**locals)
125
+ @rendered_template ||= (
126
+ #puts "\n#{self.class.name} '#{name}' calling render_template with tilt_template: #{tilt_template&.file}, engine: #{template_engine}, locals: #{locals}"
127
+ context.render_ruby_template(tilt_template, template_engine:template_engine(false), locals:locals.merge(vo:self, vue_object:self)) if !tilt_template.to_s.empty?
128
+ )
129
+ end
130
+
131
+ # Parses a rendered sfc file.
132
+ # Returns [nil, template-as-html, nil, script-as-js].
133
+ # Must be HTML (already rendered from ruby template).
134
+ def parse_sfc(**locals)
135
+ @parsed_sfc ||= (
136
+ #rendered_template = render_template(**locals)
137
+ rslt = {}
138
+ rslt[:template], rslt[:script] = render_template(**locals).to_s.match(/(.*<template>(.*)<\/template>)*.*(<script>(.*)<\/script>)/m).to_a.values_at(2,4)
139
+ rslt
140
+ )
141
+ end
142
+
143
+ def parsed_template(**locals)
144
+ @parsed_template ||= (
145
+ parse_sfc(**locals)[:template]
146
+ )
147
+ end
148
+
149
+ def parsed_script(**locals)
150
+ @parsed_script ||= (
151
+ parse_sfc(**locals)[:script]
152
+ )
153
+ end
154
+
155
+ def context
156
+ repo.context
157
+ end
158
+
159
+
160
+ ### Called from user-space by vue_app, vue_root, vue_component.
161
+
162
+ # Gets a defined wrapper, and interpolates it with the given locals & options.
163
+ def wrapper(wrapper_name, **locals) #, **wrapper_options)
164
+ #Vue::Helpers.send(wrapper_name).interpolate(**wrapper_options.merge(locals))
165
+ Vue::Helpers.send(wrapper_name).interpolate(locals)
166
+ end
167
+
168
+ def js_var_name
169
+ name.to_s.camelize
170
+ end
171
+
172
+ # def is_set?(setting)
173
+ # case
174
+ # when !send(setting).nil?; send(setting)
175
+ # when !root.send(setting).nil?; root.send(setting)
176
+ # when !Vue::Helpers.send(setting).nil?; Vue::Helpers.send(setting)
177
+ # end
178
+ # end
179
+
180
+ # Get attribute, considering upstream possibilities.
181
+ def get_attribute(attribute, use_default=true)
182
+ case
183
+ when
184
+ ( val = instance_variable_get("@#{attribute}"); !val.nil? );
185
+ val
186
+ when
187
+ type != 'root' &&
188
+ root.respond_to?(attribute) &&
189
+ ( val = root.send(attribute, use_default); !val.nil? );
190
+ val
191
+ when
192
+ use_default &&
193
+ Vue::Helpers.respond_to?(attribute) &&
194
+ ( val = Vue::Helpers.send(attribute); !val.nil? );
195
+ val
196
+ end
197
+ end
198
+
199
+ end # VueObject
200
+ end # Helpers
201
+ end # Vue
@@ -0,0 +1,49 @@
1
+ require_relative 'utilities'
2
+ require_relative 'helper_refinements'
3
+ require_relative 'vue_component'
4
+ require_relative 'vue_root'
5
+
6
+ module Vue
7
+
8
+ #Debug = []
9
+
10
+ module Helpers
11
+ using CoreRefinements
12
+ using HelperRefinements
13
+
14
+ # Stores ruby representations of vue objects.
15
+ # Intended for a single request cycle.
16
+ # TODO: Rename this to VueStash.
17
+ # NOTE: Always use the repository interface for vue-object crud operations from controller or views.
18
+ class VueRepository < Hash
19
+ attr_reader :context
20
+
21
+ # Always pass a context when creating a VueRepository.
22
+ def initialize(context)
23
+ #Debug << self
24
+ @context = context
25
+ end
26
+
27
+ # Master get_or_create for any object in the repository.
28
+ def get_or_create(klas, name, **options)
29
+ obj = fetch(name){ |n| self[name] = klas.new(name, **options.merge({repo:self})) }
30
+ obj.repo ||= self
31
+ obj.initialize_options(**options)
32
+ obj
33
+ end
34
+
35
+ # Gets or creates a VueRoot instance.
36
+ def root(name=nil, **options)
37
+ name ||= Vue::Helpers.root_name
38
+ get_or_create(VueRoot, name, **options)
39
+ end
40
+ #alias_method :[], :root
41
+
42
+ # Gets or creates a VueComponent instance.
43
+ def component(name, **options)
44
+ get_or_create(VueComponent, name, **options)
45
+ end
46
+ end
47
+
48
+ end # Helpers
49
+ end # Vue
@@ -0,0 +1,148 @@
1
+ require 'securerandom'
2
+ require_relative 'utilities'
3
+ require_relative 'helper_refinements'
4
+ require_relative 'vue_object'
5
+
6
+ module Vue
7
+ module Helpers
8
+ using CoreRefinements
9
+ using HelperRefinements
10
+
11
+ class VueRoot < VueObject
12
+ def type; 'root'; end
13
+
14
+ @defaults = {
15
+ #app_name: nil,
16
+ name: Vue::Helpers.root_name,
17
+ external_resource: nil, #Vue::Helpers.external_resource,
18
+ template_literal: nil, #Vue::Helpers.template_literal,
19
+ register_local: nil, #Vue::Helpers.register_local,
20
+ minify: nil, #Vue::Helpers.minify,
21
+ #locals: {},
22
+ }
23
+
24
+ attr_writer *defaults.keys
25
+ custom_attr_reader *defaults.keys
26
+
27
+ ### Under Construction
28
+ # Renders the html block to replace ruby vue_root tags.
29
+ #def render(tag_name=nil, locals:{}, attributes:{}, &block) # From vue_component
30
+ #def render(locals:{}, **options, &block)
31
+ def render(locals:{}, &block)
32
+ #puts "\nVueRoot#render with locals: #{locals}, self: #{self}"
33
+ #print_ivars
34
+
35
+ block_content = context.capture_html(root_name:name, locals:locals, &block) if block_given?
36
+
37
+ compiled_js = compile_app_js(locals:locals) #, **options)
38
+
39
+ root_script_output = case external_resource
40
+ # TODO: Handle external_resource:<some-string> if necessary.
41
+ when true;
42
+ key = SecureRandom.urlsafe_base64(32)
43
+ Vue::Helpers.cache_store[key] = compiled_js
44
+ callback_prefix = Vue::Helpers.callback_prefix
45
+ wrapper(:external_resource_html, callback_prefix:callback_prefix, key:key, **locals)
46
+ else
47
+ wrapper(:inline_script_html, compiled:compiled_js, **locals)
48
+ end
49
+
50
+ root_script_output.prepend(components_x_template(locals).to_s) unless template_literal
51
+
52
+ # TODO: Are locals being passed properly here?
53
+ if block_given?
54
+ wrapper(:root_app_html,
55
+ root_name: name,
56
+ block_content: block_content,
57
+ root_script_output: root_script_output,
58
+ **locals
59
+ )
60
+ else
61
+ root_script_output
62
+ end
63
+ end
64
+
65
+ def app_name
66
+ @app_name || js_var_name
67
+ end
68
+
69
+ # Gets or creates a related component.
70
+ def component(_name, **component_options)
71
+ repo.component(_name, **component_options.merge({root_name:(root_name || name)}))
72
+ end
73
+
74
+ # Selects all related components.
75
+ def components
76
+ repo.select{|k,v| v.type == 'component' && v.root_name == name}.values.compact.uniq{|v| v.name.to_s}
77
+ end
78
+
79
+ def component_names
80
+ components.map{|c| c.js_var_name}.join(', ')
81
+ end
82
+
83
+ # Returns JS string of all component object definitions.
84
+ def components_js(**component_options)
85
+ #puts "\nVueRoot#componenets_js called with components: #{components.map(&:name)}"
86
+ components.map{|c| c.to_component_js(**component_options)}.join("\n")
87
+ end
88
+
89
+ # Returns HTML string of component vue templates in x-template format.
90
+ def components_x_template(**locals)
91
+ components.map{|c| c.get_x_template(locals) unless c.template_literal}.compact.join("\n")
92
+ end
93
+
94
+ # Compiles js output (components & root) for entire vue app for this root object.
95
+ # If template_literal is false, only the js object definitions are included.
96
+ # In that case, the vue template html is left to be rendered in x-template blocks.
97
+ # TODO: Clean up args, especially locals handling.
98
+ def compile_app_js(locals:{}) #, **options)
99
+ ### Above is generic opts placeholder until we get the args/opts flow worked out.
100
+ ### It used to bee this:
101
+ # root_name = Vue::Helpers.root_name,
102
+ # file_name:root_name,
103
+ # app_name:root_name.camelize, # Maybe obsolete, see js_var_name
104
+ # #template_engine:context.current_template_engine,
105
+ # register_local: Vue::Helpers.register_local,
106
+ # minify: Vue::Helpers.minify,
107
+ # # Block may not be needed here, it's handled in 'vue_root'.
108
+ # &block
109
+
110
+ # TODO: Make these locals accessible from anywhere within the root instance,
111
+ # as we also need them for the 'render' method.
112
+ # Should this just be moved to 'render' method?
113
+ locals = {
114
+ root_name: name.to_s.kebabize,
115
+ #app_name: (options[:app_name] || js_var_name),
116
+ app_name: js_var_name,
117
+ file_name: file_name,
118
+ template_engine: template_engine(false),
119
+ components: component_names,
120
+ vue_data_json: data.to_json
121
+ }.merge!(locals)
122
+
123
+ # {block_content:rendered_block, vue_sfc:{name:name, vue_template:template, vue_script:script}}
124
+ #rendered_root_sfc_js = \
125
+ #app_js << (
126
+ #output = components_js(locals:{}, **options) << "\n" << (
127
+ output = components_js(locals:{}) << "\n" << (
128
+ parsed_script(locals) ||
129
+ wrapper(:root_object_js, **locals) #, **options)
130
+ )
131
+
132
+ #app_js << rendered_root_sfc_js
133
+
134
+ output = if minify
135
+ #extra_spaces_removed = app_js.gsub(/(^\s+)|(\s+)|(\s+$)/){|m| {$1 => "\\\n", $2 => ' ', $3 => "\\\n"}[m]}
136
+ Uglifier.compile(output, harmony:true).gsub(/\s{2,}/, ' ')
137
+ else
138
+ output
139
+ end
140
+
141
+ # Should we have an append_output option that takes a string of js?
142
+ #output << "; App = VueApp;"
143
+
144
+ end # compile_app_js
145
+
146
+ end # VueRoot
147
+ end # Helpers
148
+ end # Vue