achilles 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 51e520740708516c772ee1471e90480e4d2b13de55f0615d92fb4627a594c017
4
+ data.tar.gz: 857aade6afdccdc0651764674c2771d1af60c93e449bb62293ce8caac9d33cd6
5
+ SHA512:
6
+ metadata.gz: 0610ab82bfc5e28f191e14eaf0f780884ddd102963ff27003a106c8fb76564b8ca5aa4d5b6f4a43c20b3d8bc357897a05567cefb88c991e10c28f6d062dc27a3
7
+ data.tar.gz: 470dbfe1077dc3f10c9d57c8f2756cc597e795b098eb3adf67e01a18ce125441c8be05c071da9400d13a65ecd5c410e4f88c7cb22ac865f2829bc5d8be71e2f6
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2024 Jey Geethan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Achilles
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "achilles"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install achilles
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/achilles .css
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,4 @@
1
+ module Achilles
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Achilles
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ // Stores app level constants
2
+ var AppConstants = {
3
+ PageComponentId: 'Page'
4
+ }
5
+
6
+ export { AppConstants }
@@ -0,0 +1,90 @@
1
+ import { AppConstants } from "common/src/achilles/application/app_constants";
2
+ import { ComponentsRegistry } from "common/src/achilles/components/components_registry";
3
+ import { ComponentParser } from "common/src/achilles/components/component_parser";
4
+ import { ComponentsClassMapper } from "common/src/achilles/components/components_class_mapper";
5
+ import { Page } from "common/src/achilles/page/page";
6
+ import { Timezone } from "common/src/achilles/application/timezone/timezone";
7
+ import { Turbo } from "common/src/achilles/application/hooks-manager/turbo";
8
+ import { Observer } from "common/src/achilles/application/dom-mutation-observer/observer";
9
+
10
+ // Application class/obj to drive all things for the web app
11
+ class Application {
12
+ _page;
13
+ _engine;
14
+ _componentRegistry;
15
+ _timezone;
16
+ _componentsClassMapper;
17
+ _hooksManager;
18
+ _domMutationObserver;
19
+
20
+ constructor() {
21
+ // Set the application while creating the object
22
+ this._page = new Page(AppConstants.PageComponentId, null);
23
+ this._componentRegistry = new ComponentsRegistry();
24
+ this._componentsClassMapper = new ComponentsClassMapper();
25
+ this._timezone = new Timezone();
26
+ this._domMutationObserver = new Observer(this.setup.bind(this));
27
+
28
+ // Register the first-level special component: Page
29
+ this.componentRegistry.registerComponentByObj(this._page);
30
+
31
+ // Hook into the window events
32
+ this._hooksManager = new Turbo(this, this.setup.bind(this), this.teardown.bind(this));
33
+ }
34
+
35
+ // Getters
36
+ get componentsClassMapper() {
37
+ return this._componentsClassMapper;
38
+ }
39
+
40
+ get timezone() {
41
+ return this._timezone;
42
+ }
43
+
44
+ get componentRegistry() {
45
+ return this._componentRegistry;
46
+ }
47
+
48
+ get engine() {
49
+ return this._engine;
50
+ }
51
+
52
+ get page() {
53
+ return this._page;
54
+ }
55
+
56
+ // Setters
57
+ set engine(engine) {
58
+ this._engine = engine;
59
+ }
60
+
61
+ setup() {
62
+ this._domMutationObserver.stop();
63
+ this.parseHtmlAndRegisterComponents();
64
+ this.componentRegistry.callSetupForComponent(AppConstants.PageComponentId);
65
+ this._domMutationObserver.start();
66
+ }
67
+
68
+ teardown() {
69
+ this._domMutationObserver.stop();
70
+ // Call Page and its subcomponents beforeRenders
71
+ this.componentRegistry.callTeardownForComponent(AppConstants.PageComponentId);
72
+ // Remove/deregister all components from page except Page
73
+ this.deregisterAllComponentsExceptPage();
74
+ this._domMutationObserver.start();
75
+ }
76
+
77
+ parseHtmlAndRegisterComponents() {
78
+ const componentParser = new ComponentParser(this._componentRegistry, this._componentsClassMapper);
79
+ componentParser.parse();
80
+ }
81
+
82
+ deregisterAllComponentsExceptPage() {
83
+ let pageComponent = this.componentRegistry.getRegisteredComponent(AppConstants.PageComponentId)
84
+ pageComponent.subComponents.forEach((subComponentId) => {
85
+ this.componentRegistry.deregisterComponent(subComponentId);
86
+ })
87
+ }
88
+ }
89
+
90
+ export { Application };
@@ -0,0 +1,28 @@
1
+ class Observer {
2
+ _mutationObserver;
3
+ _callback;
4
+
5
+ constructor(callback) {
6
+ this._callback = callback;
7
+ this._mutationObserver = new MutationObserver(this.domChangedCallback.bind(this));
8
+ }
9
+
10
+ config() {
11
+ return { attributes: false, childList: true, subtree: true };
12
+ }
13
+
14
+ start() {
15
+ // Listen on html instead of body since turbo replaces body and the observer stops after one page transition
16
+ this._mutationObserver.observe($('html')[0], this.config());
17
+ }
18
+
19
+ stop() {
20
+ this._mutationObserver.disconnect();
21
+ }
22
+
23
+ domChangedCallback(mutationsList, observer) {
24
+ this._callback();
25
+ }
26
+ }
27
+
28
+ export { Observer }
@@ -0,0 +1,35 @@
1
+ class Turbo {
2
+ _application;
3
+ _setupCallback;
4
+ _teardownCallback;
5
+
6
+ constructor(application, setupCallback, teardownCallback) {
7
+ this._application = application;
8
+ this._setupCallback = setupCallback;
9
+ this._teardownCallback = teardownCallback;
10
+
11
+ this.setupEvents();
12
+ }
13
+
14
+ // Setups relevant hooks to the page for component lifecycles. This depends on the framework being used.
15
+ // Here we are using turbo drive, so hooking into that.
16
+ setupEvents() {
17
+ // Events registering
18
+ // Turbolinks lifecycle ref: https://sevos.io/2017/02/27/turbolinks-lifecycle-explained.html
19
+ // Render is not called on initial page load. So execute only once during page load
20
+ $(document).one("turbo:load", () => {
21
+ this._setupCallback();
22
+ });
23
+
24
+ // Render is called before each page transition. But its not called on initial page load. That's why we need 'load'
25
+ $(document).on("turbo:render", () => {
26
+ this._setupCallback();
27
+ });
28
+
29
+ $(document).on("turbo:before-render", () => {
30
+ this._teardownCallback();
31
+ });
32
+ }
33
+ }
34
+
35
+ export { Turbo }
@@ -0,0 +1,18 @@
1
+ class Timezone {
2
+ _timezoneString;
3
+
4
+ constructor() {
5
+ this.getTimezoneFromHtml();
6
+ }
7
+
8
+ // Getters
9
+ get timezoneString() {
10
+ return this._timezoneString;
11
+ }
12
+
13
+ getTimezoneFromHtml() {
14
+ this._timezoneString = $("div[data-app-timezone]").data("app-timezone") || "Etc/UTC";
15
+ }
16
+ }
17
+
18
+ export { Timezone }
@@ -0,0 +1,29 @@
1
+ class ComponentBase {
2
+ parentComponentId;
3
+ id;
4
+ defaultParams;
5
+ setupExecuted = false;
6
+ teardownExecuted = false;
7
+
8
+ constructor(id, parentComponentId = 'Page', defaultParams = []) {
9
+ this.id = id;
10
+ this.parentComponentId = parentComponentId;
11
+ this.defaultParams = defaultParams;
12
+
13
+ if(typeof id === 'undefined')
14
+ throw('id cannot be undefined');
15
+ }
16
+
17
+ setup() {}
18
+ teardown() {}
19
+
20
+ rootElement() {
21
+ return $(this.rootElementSelector());
22
+ }
23
+
24
+ rootElementSelector() {
25
+ return `#${this.id}`;
26
+ }
27
+ }
28
+
29
+ export { ComponentBase }
@@ -0,0 +1,36 @@
1
+ import { AppConstants } from "common/src/achilles/application/app_constants";
2
+
3
+ // Parse the entire html dom and register components if not already registered
4
+ class ComponentParser {
5
+ constructor(componentRegistry, componentsClassMapper) {
6
+ this._componentRegistry = componentRegistry;
7
+ this._componentsClassMapper = componentsClassMapper;
8
+ }
9
+
10
+ parse() {
11
+ [...$('*[data-component-class]')].forEach((elem) => {
12
+ let klassName = $(elem).data('component-class');
13
+ if(klassName.trim() === '') { return; }
14
+
15
+ let klass = this._componentsClassMapper.getComponentClass(klassName);
16
+ if(typeof klass === 'undefined' || klass === null) {
17
+ console.error(`Component class not found: ${klassName} | Element:`);
18
+ console.error($(elem));
19
+ return;
20
+ }
21
+ if($(elem).data('component-registered') === true) {
22
+ return;
23
+ }
24
+ try {
25
+ let obj = new klass($(elem).attr('id'), AppConstants.PageComponentId)
26
+ this._componentRegistry.registerComponentByObj(obj);
27
+ } catch (e) {
28
+ console.error(`Error parsing component. className: ${klassName} | Element ID: ${$(elem).attr('id')}`);
29
+ console.error($(elem));
30
+ console.error(e);
31
+ }
32
+ })
33
+ }
34
+ }
35
+
36
+ export { ComponentParser };
@@ -0,0 +1,14 @@
1
+ // Single Responsibility: Contains the list of all view components so it can be instantiated
2
+ class ComponentsClassMapper {
3
+ _componentsClassMapper = {};
4
+
5
+ addComponentClass(key, klass) {
6
+ this._componentsClassMapper[key] = klass;
7
+ }
8
+
9
+ getComponentClass(key) {
10
+ return (this._componentsClassMapper[key] || null);
11
+ }
12
+ }
13
+
14
+ export { ComponentsClassMapper }
@@ -0,0 +1,111 @@
1
+ import { AppConstants } from "common/src/achilles/application/app_constants";
2
+
3
+ // Contains all the registered components in a page at the current moment
4
+ class ComponentsRegistry {
5
+ _registeredComponents = {};
6
+
7
+ registerComponentByObj(obj) {
8
+ this.registerComponent(obj.id, obj, obj.defaultParams, obj.parentComponentId);
9
+ }
10
+
11
+ registerComponent(id, obj, defaultParams, parentComponentId) {
12
+ if($(`[id=${id}]`).length > 1) {
13
+ console.error(`Error while registering component: There are more than one elements with the same id: ${id}. Skipping registering component`);
14
+ return;
15
+ }
16
+ if(this._registeredComponents[id]) {
17
+ // Component is already registered. So have to call deregister and teardown
18
+ this.teardownAndDeregister(id);
19
+ }
20
+
21
+ this._registeredComponents[id] = {
22
+ id: id,
23
+ obj: obj,
24
+ defaultParams: defaultParams,
25
+ parentComponentId: parentComponentId,
26
+ subComponents: []
27
+ };
28
+ if(parentComponentId != null) {
29
+ let parentComponent = this.getRegisteredComponent(parentComponentId);
30
+ parentComponent.subComponents.push(id);
31
+ }
32
+ $(`#${id}`).attr('data-component-registered', 'true');
33
+ }
34
+
35
+ deregisterComponent(id) {
36
+ let component = this._registeredComponents[id];
37
+ if(!component)
38
+ return;
39
+
40
+ let parentComponent = this.getRegisteredComponent(component.parentComponentId);
41
+
42
+ if(parentComponent != null){
43
+ parentComponent.subComponents = parentComponent.subComponents.filter(item => item !== id);
44
+ }
45
+
46
+ this._registeredComponents[id] = null;
47
+ $(`#${id}`).removeAttr('data-component-registered');
48
+ }
49
+
50
+ getRegisteredComponent(id) {
51
+ return this._registeredComponents[id];
52
+ }
53
+
54
+ callSetupForComponent(id) {
55
+ let component = this.getRegisteredComponent(id);
56
+ if(!component || !component.obj)
57
+ return;
58
+ if(id !== AppConstants.PageComponentId && $('#' + id).length === 0) {
59
+ this.elementNotFound(id);
60
+ return;
61
+ }
62
+
63
+ // Call the objs default setup if its not executed already, if not skip to their children
64
+ if(component.obj.setup && component.obj.setupExecuted === false) {
65
+ try{
66
+ component.obj.setup(...component.defaultParams);
67
+ component.obj.setupExecuted = true;
68
+ } catch(e) {
69
+ console.error(e);
70
+ }
71
+ }
72
+
73
+ // Call setup for all subcomponents
74
+ component.subComponents.forEach((subComponentId) => {
75
+ this.callSetupForComponent(subComponentId);
76
+ });
77
+ }
78
+
79
+ callTeardownForComponent(id) {
80
+ let component = this.getRegisteredComponent(id);
81
+ if(!component || !component.obj)
82
+ return;
83
+
84
+ // Call the objs default teardown if not already executed, otherwise skip to its children
85
+ if(component.obj.teardown && component.obj.teardownExecuted === false) {
86
+ try{
87
+ component.obj.teardown(...component.defaultParams);
88
+ component.obj.teardownExecuted = true;
89
+ } catch(e) {
90
+ console.error(e);
91
+ }
92
+ }
93
+
94
+ // Call teardown for all sub view_components
95
+ component.subComponents.forEach((subComponentId) => {
96
+ this.callTeardownForComponent(subComponentId);
97
+ });
98
+ }
99
+
100
+ teardownAndDeregister(id) {
101
+ this.callTeardownForComponent(id);
102
+ this.deregisterComponent(id);
103
+ }
104
+
105
+ elementNotFound(id) {
106
+ console.error('Cannot find element while setup, so teardown & deregister. id: ' + id);
107
+ this.teardownAndDeregister(id);
108
+ }
109
+ }
110
+
111
+ export { ComponentsRegistry }
@@ -0,0 +1,5 @@
1
+ import { ComponentBase } from "common/src/achilles/components/component_base";
2
+
3
+ class Page extends ComponentBase {}
4
+
5
+ export { Page }
@@ -0,0 +1,4 @@
1
+ module Achilles
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Achilles
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Achilles
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Achilles</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <%= stylesheet_link_tag "achilles/application", media: "all" %>
9
+ </head>
10
+ <body>
11
+
12
+ <%= yield %>
13
+
14
+ </body>
15
+ </html>
@@ -0,0 +1,3 @@
1
+ # Pin npm packages by running ./bin/importmap
2
+
3
+ pin_all_from File.expand_path("../app/javascript", __dir__)
data/config/routes.rb ADDED
@@ -0,0 +1,2 @@
1
+ Achilles::Engine.routes.draw do
2
+ end
@@ -0,0 +1,16 @@
1
+ module Achilles
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Achilles
4
+
5
+ # Importmap initializers
6
+ initializer 'achilles.importmap', before: 'importmap' do |app|
7
+ app.config.importmap.paths << Engine.root.join('config/importmap.rb')
8
+ app.config.importmap.cache_sweepers << Engine.root.join('app/javascript') # Required to ensure that js reloads
9
+ end
10
+
11
+ # Add javascript to precompile paths
12
+ initializer 'achilles.precompile' do |app|
13
+ app.config.assets.paths << Engine.root.join('app/javascript')
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module Achilles
2
+ VERSION = "0.1.0"
3
+ end
data/lib/achilles.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "achilles/version"
2
+ require "achilles/engine"
3
+
4
+ module Achilles
5
+ # Your code goes here...
6
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :achilles do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: achilles
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jey Geethan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-06-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 7.0.2.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 7.0.2.3
27
+ description: A simple js library to make your turbo apps work better
28
+ email:
29
+ - jey@jeygeethan.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - app/assets/config/achilles_manifest.js
38
+ - app/assets/stylesheets/achilles/application.css
39
+ - app/controllers/achilles/application_controller.rb
40
+ - app/helpers/achilles/application_helper.rb
41
+ - app/javascript/achilles/application/app_constants.js
42
+ - app/javascript/achilles/application/application.js
43
+ - app/javascript/achilles/application/dom-mutation-observer/observer.js
44
+ - app/javascript/achilles/application/hooks-manager/turbo.js
45
+ - app/javascript/achilles/application/timezone/timezone.js
46
+ - app/javascript/achilles/components/component_base.js
47
+ - app/javascript/achilles/components/component_parser.js
48
+ - app/javascript/achilles/components/components_class_mapper.js
49
+ - app/javascript/achilles/components/components_registry.js
50
+ - app/javascript/achilles/page/page.js
51
+ - app/jobs/achilles/application_job.rb
52
+ - app/mailers/achilles/application_mailer.rb
53
+ - app/models/achilles/application_record.rb
54
+ - app/views/layouts/achilles/application.html.erb
55
+ - config/importmap.rb
56
+ - config/routes.rb
57
+ - lib/achilles.rb
58
+ - lib/achilles/engine.rb
59
+ - lib/achilles/version.rb
60
+ - lib/tasks/achilles_tasks.rake
61
+ homepage: https://github.com/RocketApex/achilles
62
+ licenses:
63
+ - MIT
64
+ metadata:
65
+ allowed_push_host: https://rubygems.org
66
+ homepage_uri: https://github.com/RocketApex/achilles
67
+ source_code_uri: https://github.com/RocketApex/achilles
68
+ changelog_uri: https://github.com/RocketApex/achilles
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubygems_version: 3.2.3
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: Js library as an alternative to stimulus
88
+ test_files: []