Skip to content

Commit b0e6105

Browse files
authored
Merge pull request #1 from imdrasil/feature/enhance_view_model_object
Enhance view model object
2 parents b463b0b + 6591e41 commit b0e6105

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+900
-514
lines changed

README.md

Lines changed: 103 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ViewModel [![Build Status](https://travis-ci.org/imdrasil/view_model.cr.svg)](https://travis-ci.org/imdrasil/view_model.cr)
22

3-
ViewModel pattern implementation with simple and effective form builder.
3+
ViewModel pattern implementation with simple and effective form builder and view helpers.
44

55
## Installation
66

@@ -18,138 +18,176 @@ It uses `kilt` for template rendering so you also need to add template engine yo
1818

1919
### ViewModel
2020

21-
Putting page rendering into action class (like in kemalyst) could provide to creating fat helpers (like in rails) or putting a lot of view logic into itself or template. So much more suitable alternative will be have separate class which knows how to render corresponding object for some case (action or part of a page) and includes all representation logic.
21+
Putting page rendering into action class ends with having fat helpers (like in rails) or putting a lot of view logic inside of templates. Also lack of native reusability in kilt makes you to define local variables with right name to be able to reuse them in a partials. Therefor much more suitable alternative is to have a separate class which encapsulates specific logic for a corresponding view. For such purpose this shard is created.
2222

2323
To do that load ViewModel
24+
2425
```crystal
2526
require "view_model"
2627
require "kilt/slang" # or any other template engine supported by kilt
2728
```
2829

29-
Adds `views` folder under your `src` where all classes and templates will be defined. Also root location could be customized:
30+
Create a base view class:
3031

3132
```crystal
32-
ViewModel::Config.view_path "spec/fixtures"
33+
class ApplicationView < ViewModel::Base
34+
end
3335
```
3436

35-
Here is example of folders structure:
37+
By default layout path is `"src/views/layouts/layout.slang"` but this can be easily redefined by `.layout` macro:
3638

39+
```crystal
40+
class ApplicationView < ViewModel::Base
41+
layout "app/views/layouts/layout"
42+
end
3743
```
38-
src
39-
├── views
40-
│ ├── comment
41-
│ │ ├── index.slang
42-
│ │ ├── show.slang
43-
│ ├── post
44-
│ │ ├── index.slang
45-
│ ├── comment_view.cr
46-
│ ├── post_view.cr
44+
45+
> Pay special attention - layout path doesn't include file extension.
46+
47+
If you'd like to render your view without a layout - pass `false` as an argument.
48+
49+
Next define specified layout:
50+
51+
```slang
52+
html
53+
head
54+
title Page title
55+
body
56+
- yield_content
4757
```
4858

49-
All view models are named like `<model name>_view.cr`. Here is example of such classes:
59+
`yield_content` macro is just a alias for `yield(__kilt_io__)` - it yields `IO` to view `#content` method which renders content.
60+
61+
Now we can specify a view class.
5062

5163
```crystal
52-
class CommentView < ViewModel::Base
53-
def_template index
54-
def_template show
55-
end
64+
# src/views/posts/show_view.cr
65+
module Posts
66+
class ShowView < ViewModel::Base
67+
model post : Post
5668
57-
class PostView < ViewModel::Base
58-
template_path "spec/fixtures"
59-
getter model : Post
60-
61-
delegate some_method, to: model
62-
63-
def_template index do
64-
template + "123"
69+
delegate :title, :content, to: post
6570
end
6671
end
6772
```
6873

69-
To create object of view-model you should pass object being rendered to constructor
74+
`.model` macro creates getter for given attributes and generates constructor accepting them.
7075

71-
```crystal
72-
c = Comment.new
73-
CommentView.new(c)
76+
Content for a post object:
77+
78+
```slang
79+
.header
80+
h3 = title
81+
.content
82+
= content
7483
```
75-
By default view-model will suppose that it will get object of class named same as it but without "View" part. This could be override adding to class definition direct declaration of `getter model : Post`. `model` - getter which give access to passed object. Also template location path for particular class could be redefined inside of it.
7684

77-
To declare actions that could be rendered use `def_template` macros which accepts name of action and optional block. Template for action will be loaded from file named after action located inside of the folder named after it at the same level.
85+
By a convention this template file should be located in `<view_class_folder>/<view_class_name_without_view/content.slang>`, in our case it will be `src/views/posts/show/content.slang`.
7886

79-
Given block could access the rendered template via `template` local variable.
87+
For a view rendering `.view` macro can be used - just pass view name and required arguments:
8088

81-
Render action of such view-model anywhere is easy - just add:
8289
```crystal
83-
c = Comment.new
84-
view("comment", :index, c)
90+
view("posts/show", post)
91+
# or for a collection
92+
collection_view("posts/show", posts)
8593
```
8694

87-
`view` macros accepts 3 arguments: view-model name (without view part), action and object to represent.
95+
### Partials
8896

89-
To render collection use
97+
If you would like to define some shared templates or separate your view into several partials use `.def_partial` macro:
9098

9199
```crystal
92-
c = [Comment.new, Comment.new]
93-
collection_view(:comment, :index, c)
100+
module SharedPartials
101+
include ViewModel::Helpers
102+
103+
def_partial button, color
104+
end
105+
106+
module Posts
107+
class ShowView < ApplicationView
108+
include SharedPartials
109+
110+
def_partial body
111+
end
112+
end
94113
```
95114

96-
It will render each given object and concatenate all results.
115+
If you need to define a module with partials - include `ViewModel::Helpers` module into it. `.def_partial` accepts partial name as a first argument and partial arguments as all others. All partial template paths are calculated same was as for content files of view objects. The only difference is that partial files name has a `_` symbol prefix: `src/views/shared_partials/_button.slang`.
97116

98-
### Html helpers
117+
To render a partial use `.render_partial` macro:
99118

100-
Also this shard provided html generating helper methods for some tags. They look like in the rails `action_view` but because of crystal template rendering mechanism works in slightly in another way.
119+
```slang
120+
.buttons
121+
- render_partial :button, :read
122+
```
101123

102-
There are two modules:
103-
- `Helers` - includes all methods to generate tags and form;
104-
- `Macrosses` - includes macrosses to simplify calling helper methods with string builder inside of view (useful inside of templates.
124+
### Html helpers
105125

106-
All helper methods have 2 variant: using given string builder to append content and just returning string. First one are much more effective so prefer to use it.
126+
Also this shard provides HTML helper methods. All methods are automatically included in `ViewModel::Base`.
107127

108128
Methods description:
109-
- `form_tag` - yields `FormBuilder` to generate flexible forms; append `_method` hidden input if given method is not `get` or `post`
110-
- `content_tag` - builds given tag; could accept block
111-
- `link_to` - builds `a` tag
129+
130+
- `content_tag` - builds given tag with given options; could accept block for nested content
131+
- `link_to` - builds link
112132
- `label_tag` - builds `label`
113-
- `select_tag` - builds `select` tag; automatically generates `option` for given array
114-
- `text_area_tag` - builds `text_are`
133+
- `select_tag` - builds `select` tag; automatically generates `option` tags for given array
134+
- `text_area_tag`
115135
- `hidden_tag`
116136
- `text_tag`
117137
- `submit_tag`
118138
- `file_tag`
119139
- `password_tag`
140+
- `email_tag`
120141
- `checkbox_tag`
121142
- `radio_tag`
122143
- `time_tag`
123144
- `date_tag`
124145
- `number_tag`
125146

126-
All macrosses are named after correspond methods with adding `_for` at the end.
127-
128147
#### FormBuilder
129148

130-
To build form with automatically generated names and ids of elements:
149+
To build form with automatically generated names and ids of inputs :
131150

132-
```slim
133-
- form_tag_for(:some_form, "/posts", :post) do |f|
151+
```slang
152+
- build_form(:some_form, "/posts", :post) do |f|
134153
p here could be some other html
135154
div
136-
- f.text_field :name
155+
- f.text_field :name
137156
- f.select_field :tag, [[1, "crystal"], [2, "ruby"]], 1
138157
- f.submit "Save"
139158
```
140159

141-
Because crystal loads template and build it inside of methods using `String::Builder` to generate content `-` should be used instead of `=` - content will be added to builder inside of method so there is no need to do it manually.
160+
`.build_form` macro creates `ViewModel::FormBuilder` and passes it to the block. Form builder provides a set of methods similar to ones described above. All inputs will get own id and class based on it's name.
161+
162+
All form builder methods manipulate `__kilt_io__` directly and returns `nil` so it isn't important the way to call them: with `-`, `=` or `==`.
163+
164+
If you specify a form method different from `get` and `post` - form builder will add additional hidden input with name `_method` for the given method and set current form method to `post`.
165+
166+
### link_to
167+
168+
Also HTML helper includes `.link_to` macro. It allows to generate `<a>` tag with all needed data.
169+
170+
```slang
171+
== link_to "Show", "/posts/23", { "class" => "special-link" }
172+
173+
== link_to "/order/12" do
174+
span
175+
b Open
176+
```
177+
178+
If you want to make a link to do a non-GET request (e.g. delete button), you can specify `method` argument and additionally load `libs/view_model/assets/view_model.js` file.
179+
180+
```slang
181+
== link_to "delete", "/comments/56", :delete
182+
```
142183

143184
## Development
144185

145186
There are still a lot of work to do. Tasks for next versions:
146187

147-
- [ ] add spec2 matchers
148-
- [ ] rewrite to use spec2
188+
- [ ] add spec matchers
149189
- [ ] add more html helpers
150-
- [ ] add `multiple` support for select
151190
- [ ] add array support in name generation
152-
- [ ] add `button_to`
153191

154192
## Contributing
155193

@@ -163,7 +201,7 @@ Please ask me before starting work on smth.
163201

164202
Also if you want to use it in your application (for now shard is almost ready for use in production) - ping me please, my email you can find in my profile.
165203

166-
To run test use regular `crystal spec`.
204+
To run test use regular `crystal spec`.
167205

168206
## Contributors
169207

assets/view_model.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Allows delete links to post for security and ease of use similar to Rails jquery_ujs
3+
*
4+
* amber - v0.11.1 - 2018-11-07
5+
* https://github.com/amberframework/amber
6+
* Copyright (c) 2017-2018 Amber Team
7+
*/
8+
document.addEventListener("DOMContentLoaded", function () {
9+
var elements = document.querySelectorAll("a[data-method='delete']");
10+
var i;
11+
for (i = 0; i < elements.length; i++) {
12+
elements[i].addEventListener("click", function (e) {
13+
e.preventDefault();
14+
var message = e.target.getAttribute("data-confirm") || "Are you sure?";
15+
if (confirm(message)) {
16+
var form = document.createElement("form");
17+
var input = document.createElement("input");
18+
form.setAttribute("action", e.target.getAttribute("href"));
19+
form.setAttribute("method", "POST");
20+
input.setAttribute("type", "hidden");
21+
input.setAttribute("name", "_method");
22+
input.setAttribute("value", "DELETE");
23+
form.appendChild(input);
24+
document.body.appendChild(form);
25+
form.submit();
26+
}
27+
return false;
28+
})
29+
}
30+
});

shard.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
name: view_model
2-
version: 0.1.0
2+
version: 0.2.0
33

44
authors:
55
- Roman Kalnytskyi <moranibaca@gmail.com>
66

7-
crystal: 0.21.1
7+
crystal: 0.27.0
88

99
license: MIT
10-
dependencies:
11-
kilt:
10+
dependencies:
11+
kilt:
1212
github: jeromegn/kilt
1313
version: ~> 0.4
1414
development_dependencies:

spec/fixtures/base_view.cr

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
abstract class BaseView < ViewModel::Base
2+
layout "spec/fixtures/layouts/layout"
3+
end

spec/fixtures/comment/index.slang

Lines changed: 0 additions & 8 deletions
This file was deleted.

spec/fixtures/comment_view.cr

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
div some text
2+
h2 some title
3+
== content_tag(:span) do
4+
== link_to "/all", "Text"
5+
== label_tag "Me"
6+
== select_tag(:dropdown, [[1, "v1"], [2, "v2"]])
7+
== text_area_tag(:text, "some value")
8+
== hidden_tag(:hidden_field, "hidden1")
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module Comments
2+
class IndexView < BaseView
3+
include SharedPartials
4+
5+
model comment : Comment
6+
end
7+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
require "./show_view"
2+
3+
module Comments
4+
class InheritedShowView < ShowView
5+
end
6+
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
div New Body

0 commit comments

Comments
 (0)