Xadmin控件的实现:〇九添加、修改视图的pop功能
今天开始讲一个新的功能——POP。先看一下admin组件的POP功能效果
整个思路就是在添加或修改数据的时候,在一对多或者多对多的字段旁边有个加号,点击以后会弹出一个对话框用来添加新的数据,而不用专门进入到对应的publisher表或者authors表对应的添加页面。
添加完数据以后点击确定返回主页面,对应的字段的下拉控件里被选中的就是我们新添加的数据。总结一下就是下面三条:
- 在一对多或多对多的字段渲染input标签的时候后面加上一个内容为+的a标签
- 给a标签设置相应的跳转URL并实现弹窗的效果
- 保存记录,将原页面的下拉菜单中添加该记录并且显示为选中的状态
首先我们先让页面实现弹窗的效果
window.open
弹窗主要是用了BOM的window对象。我们直接放一段HTML的代码来演示一下
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Document</title> 7 </head> 8 <body> 9 <p><button onclick="foo()">+</button></p> 10 </body> 11 <script> 12 function foo(){ 13 window.open('https://www.baidu.com/',"","width=200px,width=100px") 14 }; 15 </script> 16 </html>
在页面上放一个按钮,按钮关联了一个函数,函数里就放了一个window.open方法,先看看效果
window.open里的参数
在上面的例子里window.open里我只用到了两个参数,但是默认情况下是有四个参数的。
window.open(URL,name,specs,replace)
参数 | 说明 | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
URL | 可选。打开指定的页面的URL。如果没有指定URL,打开一个新的空白窗口 | ||||||||||||||||||||||||||||
name | 可选。指定target属性或窗口的名称。支持以下值:
|
||||||||||||||||||||||||||||
specs | 可选。一个逗号分隔的项目列表。支持以下值:
|
||||||||||||||||||||||||||||
replace | Optional.Specifies规定了装载到窗口的 URL 是在窗口的浏览历史中创建一个新条目,还是替换浏览历史中的当前条目。支持下面的值:
|
上面的表格里列举了几乎所有我们常用的参数,在前面的代码中,我们只用了url和specs两个参数(注意两个参数之间是有个空字符串的)。
我们现在做两个页面,然后把路由设置好
页面一上放一个input标签还有一个+按钮,页面2上放一个form表单,里面放一个input标签还有一个submit标签
页面1就是刚才讲的弹出对页面的页面,要做的就是从1弹出2,然后再2里写入数据然后关闭页面最后显示在1里的input框里。
下面是第一个页面的代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <script src="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <title>Document</title> </head> <body> <input type="text"> <p><button onclick="foo()">+</button></p> </body> <script> function foo(){ window.open('/pop/',"","width=200px,width=100px") } </script> </html>
还有弹出的第二个页面的代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <form action="" method="POST"> {%csrf_token%} 提交 <input type="text" name='title'> <input type="submit" onclick="foo()"> </form> </body> </html>
路由和视图就先不写了,直接映射过来就好了。
子页面的关闭
子页面要怎么关闭呢?
子页面的关闭是个挺有意思的方法,如果我们只是把window.close()和submit的按钮关联在一起的时候,子页面是可以关闭的,但是如果把pop的URL手动输入以后,点击按钮是没法关闭页面的。而这个时候也是要考虑到的,这里用了一个另外的方法,看一下pop对应的视图
def pop(request): if request.method == 'POST': data = request.POST.get('title') return render(request,'win_close.html') else: return render(request,'pop.html')
然后专门写一个html页面,里面只放一个js指令
<!-- 文件名:win_close.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> </body> <script> window.close() </script> </html>
一旦点击了submit提交了表单,在视图里render了这个win_close页面,直接关闭页面。而这个win_close页面我们没有专门指定URL,是在什么时候都不会被主动渲染的。
数据的传递
在点击了submit以后我们需要在页面1里的input里显示出被提交的数据。
首先我们要知道这两个页面是有父子关系的,页面1是页面2的父级页面,那么有个方法是用来调用父级页面里的函数的js语法
window.opener.function(args)
那么就可以这样搞
给页面1(open.html)添加一个js函数
function bar(data){ console.log(data) }
然后给win_close.html添加一个语句
window.opener.bar("{{data}}");
一定注意变量要用引号引起来!!!!!!
然后给pop视图里render后面的加个locals()的方法作为参数让页面能拿到我们POST的数据
这样就在主页面里拿到了子页面里的数据。最后用jquery的选择器加上显示效果就行了。
function bar(data){ $('#data').val(data) }
对了,在页面1的代码里没有定义input标签的id,加上id='data'就行了。参数写的有点乱,没加引号的data是传递的参数,而#data则是id选择器。
这里要注意的是页面和页面的关系,其实说页面不太对,应该是窗口和窗口的关系。我们是通过open.html这个窗口打开的子窗口,子窗口里先后刷新了两个html页面,所以win_close.html也是open.html的子窗口。
这样就好了!
在完成页面的弹出效果以后,该着手处理一下前端的效果了。前端要做的就是在每个外键的字段货多对多的字段的输入框旁边加上一个+按钮。
字段的判定
我们在上一章讲filter打时候讲了字段对象——这个对象是我们在定义model的时候的一个对象。但是由于我们用来生成表单的时候用的是form,也就是form对象。form对象就是在定义form时候里面的字段。先看看下面几个类的关系
from django.db import models class Book(models.Model): title = models.CharField(max_length=10) price = models.IntegerField() from django import forms class BookForm(forms.Form): title = forms.CharField(max_length=10) price = forms.IntegerField() from django.forms import ModelForm class BookForm2(ModelForm): class Meta: model = Book filds = "__all__"
在上面的代码中我们定义了三个类:
- Book——model
- BookForm——Form
- BookForm——ModelForm
modelform的作用相当于通过代码自动转化成一个form类。也就是说BookForm2和BookForm是一样的效果,在前端直接渲染成一个input标签。
可是多对多或者一对多的外键是怎么处理的呢?比方我们前面一直用的Books类,里面包含了一个一对多的Publisher和一个多对多的authors,那么就会转换成下面的内容
class BookForm(forms.Form): title = forms.CharField(max_length=10) price = forms.IntegerField() publisher = forms.ModelChoiceField('Publisher') authors = forms.MultipleChoiceField('Authors')
注意后面两个form对象的属性,一个是ModelChoiceField 一个是MultipleChoiceField,对一个的就是一个是ForeignKey和ManyToManyField。还有个点要注意点,就是ModelChoiceField是MultipleChoiceField的一个子类,所以我们只需要判定一个form对象是不是MultipleChoiceField的对象就可以知道他是不是一对多或者多对多。然后给他赋个值
def add_view(self,request): ModelFormDemo = self.get_modelform_class() form = ModelFormDemo() #pop功能 from django.forms.models import ModelChoiceField for field in form: if isinstance(field.field,ModelChoiceField): field.is_pop = True
form是吧一个对象直接通过locals()传递给前端的,前端通过for循环生成一个个字段对应的标签。在循环的时候可以用if判断is_pop的状态,如果该字段是一对多或多对多,就生成一个a标签
<form action="" method="post"> {%csrf_token%} {%for field in form%} <div> <label for="">{{field.label}}</label> </div> {{field}} {%if field.is_pop%} <a onclick="pop('{{field.url}}')"><h3>+</h3></a> {%endif%} {%endfor%} <button class="btn btn-success pull-right">提交</button> </form>
上面的代码只是实现了功能,样式什么的都不讲究了。
看到后面两个字段下面的加号了吧!
url指定
回顾一下前面的增加视图,我们在视图中是通过一个ModelForm来生成一个form表单。
def add_view(self,request): ModelFormDemo = self.get_modelform_class() form = ModelFormDemo() for field in form: print(field,type(field))
通过上面的代码来演示一下,看看通过for循环打印的field的内容。
结论就不放了,field是一段html代码,对应的就是前端里form表单里的每个标签。类型很重要
<class 'django.forms.boundfield.BoundField'>
我们用下面的代码导入一下BoundField可以看一看里面有哪些方法
from django.forms.boundfield import BoundField
然后点进去,可以看看源代码
@html_safe class BoundField: "A Field plus data" def __init__(self, form, field, name): self.form = form self.field = field self.name = name self.html_name = form.add_prefix(name) self.html_initial_name = form.add_initial_prefix(name) self.html_initial_id = form.add_initial_prefix(self.auto_id) if self.field.label is None: self.label = pretty_name(name) else: self.label = self.field.label self.help_text = field.help_text or '' def __str__(self): """Render this field as an HTML widget.""" if self.field.show_hidden_initial: return self.as_widget() + self.as_hidden(only_initial=True) return self.as_widget()
通过这个主要是要拿到pop出来的页面的url,也就是我们定义的add页面,当我们点击Publisher下面的加号以后,弹出的页面应该对应的是publish/add。所以需要的就是app的名称字符串和model名字符串。然后用反转URL的方法拿到需要弹出页面的URL。
for field in form: if isinstance(field.field,ModelChoiceField): field.is_pop = True field_name = field.name #字段名字符串 related_model_name = field.field.queryset.model._meta.model_name related_app_label = field.field.queryset.model._meta.app_label _url = reverse("%s_%s_add"%(related_app_label,related_model_name)) field.url = _url+"?pop_id=id_{}".format(field.name)
和前面的is_pop一样,也是用一个属性添加在field对象中。在前端渲染的时候给a标签的href属性。
url里用了字符串的拼接,加上了一个id_publisher,是因为我们的form表单中每个控件都有id值,为了后期可以讲新添加的对象直接显示在对应的控件中,所以要拿到这个标签进行DOM操作。
现在子页面和主页面都已经好了,下面就要进行数据的操作了
add页面
由于我们队add视图进行了修改,所以响应的模板也要修改一下
1 <body> 2 <div class="container"> 3 <div class="col-md-8 col-md-offset-3"> 4 <h2>添加数据</h2> 5 <form action="" method="post"> 6 {%csrf_token%} 7 {%for field in form%} 8 <div> 9 <label for="">{{field.label}}</label> 10 </div> 11 {{field}} 12 {%if field.is_pop%} 13 <a onclick="pop('{{field.url}}')"><h3>+</h3></a> 14 {%endif%} 15 {%endfor%} 16 <button class="btn btn-success pull-right">提交</button> 17 </form> 18 </div> 19 </div> 20 </body> 21 <script> 22 function pop(url){ 23 window.open(url,"","width=600,height=400") 24 }; 25 function pop_resp(ele,pk,text){ 26 var $option = $('<option>'); 27 $option.html(text); 28 $option.val(pk); 29 $option.attr('selected','selected') 30 $(ele).append($option) 31 } 32 </script>
主要是将body和script里的代码放出来了,其中body里的一部分前面已经讲过了,在渲染主页面的时候,会通过is_pop的属性在一对多货多对多的字段旁加一个a标签,a标签的url是从视图传来的拼接以后的url,主页面是用的GET方法。
在点击了加号以后就会触发pop函数,弹出一个子页面,注意看下面的图里的url
上面就是点击了publiser下的加号弹出的子窗口。url对应的就是我们定义的添加页面,只不过后面加了个参数:这个参数就是输入控件的id
视图部分
添加视图中写入数据库的方法是POST,注意的是在POST下也可以使用GET获取到URL里的参数。
1 def add_view(self,request): 2 ModelFormDemo = self.get_modelform_class() 3 form = ModelFormDemo() 4 5 #pop功能 6 from django.forms.models import ModelChoiceField 7 8 for field in form: 9 if isinstance(field.field,ModelChoiceField): 10 field.is_pop = True 11 12 field_name = field.name #字段名字符串 13 related_model_name = field.field.queryset.model._meta.model_name 14 related_app_label = field.field.queryset.model._meta.app_label 15 16 _url = reverse("%s_%s_add"%(related_app_label,related_model_name)) 17 18 field.url = _url+"?pop_id=id_{}".format(field.name) 19 20 if request.method == 'POST': 21 form = ModelFormDemo(request.POST) 22 23 if form.is_valid(): 24 obj = form.save() 25 26 pop_id = request.GET.get('pop_id',0) 27 if pop_id: 28 res = {"pk":obj.pk,'text':str(obj),'ele':"#"+pop_id} 29 return render(request,'win_close.html',locals()) 30 else: 31 return redirect(self.get_list_url()) 32 33 34 return render(request,'add.html',locals())
注意看上面的POST的过程,先获取一个pop_id,如果没有值(0),就在写入数据库以后跳转到显示数据的页面上。否则跳转到定义的win_close.html文件。
<!-- 文件名:win_close.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> </body> <script> window.opener.pop_resp("{{res.ele}}","{{res.pk}}","{{res.text}}"); window.close() </script> </html>
页面直接定义了两个方法,后面的是关闭子页面就不说了,第一个是调用父页面的pop_rest函数。函数给了三个参数:
- ele:点击+对应的标签的id
- res.pk:新添加对象的id
- res.text:新添加对象的值
res就是我们在视图中定义的一个用来穿参数的字典。要注意点是ele的值在视图中进行了一下拼接,主要是为了匹配jquery的语法(id选择器)。
父级页面里的pop_resp方法
function pop_resp(ele,pk,text){ var $option = $('<option>'); $option.html(text); $option.val(pk); $option.attr('selected','selected') $(ele).append($option)
就是创建一个option标签,然后把标签的text和val的值按要求声明一下(text就是新添加的对象,val就是对象的pk),然后加上一个选中的效果让他显示出来,最后添加在select标签里。
最终的效果就完成了。还可以调整一下前端显示的效果,这里就不再赘述了!