Package: djvue
DjVueHandle Django forms with Vue.js and Django REST FrameworkThis project aims to help to build hybrid views that handle both templates renders as well as the REST API. Useful when the client doesn’t have a SPA for the frontend but does need mobile apps or an API or developing an API in an existing project that is required. I suggest checking theexampleapp for seeing a concrete implementation.The trade-off made is getting rid ofDjango Formsand replace them withDjango REST Framework form renderer.RequirementsDjango(2.2+)DRFFor the form validation and submission:Vue.js 2.6VeeValidateInstallationInstalldjvue(ordownload from PyPI):pipinstalldjvueAdddjvuetoINSTALLED_APPSinsettings.py:INSTALLED_APPS=(# other apps"djvue",)Enable session authentication for DRF.WarningFailing in doing this will make all your views csrf exempted.REST_FRAMEWORK={# ...'DEFAULT_AUTHENTICATION_CLASSES':('rest_framework.authentication.SessionAuthentication',# ...),# ...}If there are files to be uploaded via forms, it’s required to includedjvue.urlsin your rooturls.py:# For Django >= 2.2urlpatterns+=[path('',include('djvue.urls'))]How it worksRendering a simple formDjVueallows you to easily render straightforward forms.Define the serializer.classLoginSerializer(serializers.Serializer):email=serializers.EmailField(required=True)password=serializers.CharField(write_only=True,style={"input_type":"password"})Your views will require to return the serializer definition and the rendered template on the GET requests, exactly like Django CBVs are doing.fromrest_framework.genericsimportCreateAPIViewfromrest_framework.renderersimportTemplateHTMLRenderer,JSONRendererfromrest_framework.responseimportResponsefrom.serializersimportLoginSerializerclassLoginView(CreateAPIView):renderer_classes=[TemplateHTMLRenderer,JSONRenderer]serializer_class=LoginSerializertemplate_name="login.html"defget(self,request,*args,**kwargs):returnResponse({"serializer":self.get_serializer()})Include the prerequisites at the bottom ofbase.html{% load vue_tags %}
.... other scripts
{% vue_starter %}Vue appEach form is another Vue instance.<divid="login-app"hidden>...</div>NoteHide the div holding the Vue.js app to avoid showing to the user un-rendered HTML using thehiddenHTML attribute. It can be removed once the DOM has loaded, placed usually at the end of the script.Form definitionvalidation-observerDefine the form using VeeValidates’sValidationObservercomponent.render_vue_formRender the serializer as an HTML form usingrender_vue_form. This template tag is a wrapper around the original DRFrender_formtemplate tag which changes the field style.<validation-observerref="form"v-slot="{ handleSubmit }"mode="lazy"><form@submit.prevent="handleSubmit(submit)"novalidate="true"><divclass="row"><divclass="col-sm">{% render_vue_form serializer %}</div></div><buttonclass="btn btn-primary">Login</button></form></validation-observer>render_vue_fieldFor a more granular controlrender_vue_fieldtemplate tag can be used.<validation-observerref="form"v-slot="{ handleSubmit }"mode="lazy"><form@submit.prevent="handleSubmit(submit)"novalidate="true"><divclass="row"><divclass="col-sm">{% render_vue_field serializer.username %}
                {% render_vue_field serializer.password %}</div></div><buttonclass="btn btn-primary">Login</button></form></validation-observer>djVueMixinCreate a new Vue app and use djVueMixin which handles the form validation, file upload, and submission.Define the form fields insidedatamethodformobject. Note that you need to define manually every form field that has to be passed to the server, excepting file fields, which will cover in another example later.Mandatory implementation inputactionURL: defines where the form has to be sent via a POST request to the server.success: method is called when the server returns a success response (status 200).newVue({el:'#login-app',mixins:[djVueMixin],data(){return{actionURL:'{% url "login" %}',form:{email:null,password:null}}},methods:{success(response){window.location.href="{% url 'user:dashboard' %}"}}})// remove hiddenletappEl=document.getElementById('login-app');appEl.removeAttribute("hidden");Display the validation errorsAt this step, live validation is setup. Each form field is validated individually in the partial HTML field. It can be customized by creating a newtemplate pack. Add a placeholder anywhere on the page for rendering forms global validation error likeServer Erroror better use atoastror SnackBar.Displaying server side field errors is implemented only for one nesting level, if you need more you should overrideerrormethod fromdjVueMixin.<pv-for="error in nonFieldErrors":key="error"class="text-danger">{( error )}</p>Advanced usagefromdjvue.fieldsimportFileFieldclassWorkSerializer(serializers.Serializer):CHOICES=(("cc","Chocolate Tested"),("dreamer","Dreamer"),("sp","Smokes packing"),)job=serializers.ChoiceField(choices=CHOICES)position=serializers.CharField(required=False)classProfileSerializer(serializers.ModelSerializer):username=serializers.CharField(max_length=25,min_length=3,required=True,)email=serializers.EmailField(required=True)password1=serializers.CharField(write_only=True,style={"input_type":"password","rules":"password:@password2"},)password2=serializers.CharField(write_only=True,style={"input_type":"password"})file=FileField(required=True)working_place=WorkSerializer(write_only=True)classMeta:model=Profilefields=("username","email","password1","password2","file","working_place",)File uploadFile upload starts as soon asonchangeDOM event occurs. Behind the scene, a global view is uploading the file to a temporary location and returns to the client thepathand the originalfilenamewhich will be sent together with the form data upon submission. If you want to enforce special validation, DjVue batteries can be subclasses to create your custom logic.To enable file upload, it’s required to use DjVue’sFileFieldinstead of the default one.FileFieldA hybrid file field. Renders an input type, accepts as input a dictionary containing the filename and the file path and it serializes the representation like a native serializer.FileField.serializers.pyfromdjango.core.validatorsimportFileExtensionValidatorfromdjvue.serializersimportFileUploadSerializerclassPDFUploadSerializer(FileUploadSerializer):"""
    Allows only PDF files to be uploaded
    """def__init__(self,*args,**kwargs):super().__init__(*args,**kwargs)self.fields["file"].validators.append(FileExtensionValidator(allowed_extensions=['pdf']))views.pyfromdjvue.viewsimportFileUploadViewclassPDFUploadView(FileUploadView):serializer_class=PDFUploadSerializerurls.pyurlpatterns=[path('<scrambled-url>',PDFUploadView.as_view(),name="pdf_upload")]Once the backend is implemented, the Vue.js app is left to be updated and that’s all.newVue({// ...uploadURL:"{% url 'pdf_upload' %}"// ...})Upon form submission, the uploaded files must be linked with some model or pushed somewhere else. Let’s see a trivial example of how that can be done,filenameandpathare always returned by the view usingFileUploadSerializeranddjVueMixindoes the job of POSTing them to theactionURLtogether with the rest of the form fields.The current example considers one url for all files which belong to the same form, for handling different validations per file, each field can have it’s own upload url by definingupload_urlin field style.any_file=FileField()# uses the uploadURL defined in the Vue instancepdf=FileField(style={"upload_url":reverse_lazy("pdf_upload")})image=FileField(style={"upload_url":reverse_lazy("image_upload")})serializers.pyclassProfileSerializer(serializers.ModelSerializer):defcreate(self,validated_data):user_file=validated_data.pop("file",None)profile=Profile.objects.create(**validated_data)# # fetch the file from temporary dirifuser_fileisnotNoneandall([user_file.get("path",False),user_file.get("filename",False)]):withopen(user_file["path"],"rb")asf:profile.file.save(user_file["filename"],f)returnprofileListFileFieldImplementsmultipleupload, a list of FileField, inheriting all its features.
Once a file has been uploaded, the filename will be rendered under it.multiple_file=ListFileField()# uses the uploadURL defined in the Vue instancemultiple_pdf=ListFileField(style={"upload_url":reverse_lazy("example:pdf_upload")})serializers.pyfromdjango.core.files.baseimportContentFileclassProfileSerializer(serializers.ModelSerializer):defcreate(self,validated_data):# ...multiple_file=validated_data.pop("multiple_file",None)profile=Profile(**validated_data)profile.save()# fetch the file from temporary dirifmultiple_fileisnotNoneandall([a_file.get("path",False)anda_file.get("filename",False)fora_fileinmultiple_file]):fora_fileinmultiple_file:withopen(a_file["path"],"rb")asf:ProfileAttachment.objects.create(profile=profile,file=ContentFile(f.read(),a_file["filename"]))FieldsetsBy default, DjVue can handle also nested serializers with one nesting level, though if you need more, this behavior can be easily changed. Child serializer fields will be rendered in the same format that parent fields are. The only adjustment required to support them is to modify theformkey from thedatamethod to include an object which defines the child serializer fields rather than a key-value pair.newVue({// ...data(){return{form:{// ...working_place:{job:null,position:null}}}},// ...})FormsetsAt this moment formset are indeed supported, but they have to be written by hand. It’s on the road map to provide utilities for them also. Here’s a naive implementation of how they can be done:serializers.pyclassAddressSerializer(serializers.Serializer):COUNTRY_CHOICES=(("ro","Romania"),("de","Germany"),("kw","Kuwait"))country=serializers.ChoiceField(choices=COUNTRY_CHOICES)zip_code=serializers.CharField()address=serializers.CharField(required=False)classMeta:list_serializer_class=serializers.ListSerializerclassProfileSerializer(serializers.ModelSerializer):# ...addresses=AddressSerializer(many=True)script.jsletaddressIndex=0newVue({// ..data(){return{formsetReady:false,formsetDefinition:{},form:{// ..addresses:[{id:`address-${addressIndex}`,country:null,zip_code:null,address:null}]},}},watch:{options(){// set the formset definitionsthis.formsetDefinition=this.options.addresses.child.childrenthis.formsetReady=true}},methods:{addAddress(){addressIndex++this.form.addresses.push({id:`address-${addressIndex}`,country:null,zip_code:null,address:null,})},deleteAddress(index){this.form.addresses.splice(index,1)},}})Place the formset anywhere inside the form definition wrapped with its ownvalidation-observer.index.html<validation-observerref="addresses"><divclass="card mb-3"v-for="(address, index) in form.addresses":key="address.id"><divclass="card-body"><spanclass="float-right"style="cursor: pointer"@click="deleteAddress(index)">x</span><h4class="card-title">Address</h4><divclass="address-form"><validation-provider:name="`country-${index}`"rules="required"v-slot="{ errors, valid, invalid, validated }"tag="div"class="form-group"><selectv-model="address.country"class="form-control"name="country":class="{'is-invalid': validated && invalid, 'is-valid': valid}"><optiondisabledvalue="">Select country</option><optionv-for="opt in formsetDefinition.country.choices":value="opt.value">{( opt.display_name )}</option></select><pv-for="error in errors":key="error"class="text-danger">{( error )}</p></validation-provider><validation-provider:name="`zip_code-${index}`"rules="required"v-slot="{ errors, valid, invalid, validated }"tag="div"class="form-group"><inputv-model="address.zip_code"type="text"class="form-control mb-2":class="{'is-invalid': validated && invalid, 'is-valid': valid}"placeholder="Zip Code"><pv-for="error in errors":key="error"class="text-danger">{( error )}</p></validation-provider><validation-provider:name="`address-${index}`"rules="required"v-slot="{ errors, valid, invalid, validated }"tag="div"class="form-group"><textareav-model="address.address"type="text"class="form-control mb-2":class="{'is-invalid': validated && invalid, 'is-valid': valid}"placeholder="Address"></textarea><pv-for="error in errors":key="error"class="text-danger">{( error )}</p></validation-provider></div></div></div></validation-observer>i18n and custom field error messagesBy default, error messages are rendered in the English language. In order to change them, add insettings.pythe path of the file where new messages are located.settings.pyLANGUAGE_CODE="en-us"DJVUE_VV_LOCALE_PATH="example.locale.djvue_messages"This file must contain a dictionary that matches the language codes defined inLANGUAGESor if your project is not multilingual and if you need to override the default messages, define a dictionary with one key which is matchingLANGUAGE_CODEvalue. The children key which holds the messages must match theVeeValidate keys.djvue_messages.pyvv_locale={"en-us":{"en":{"messages":{"alpha":"This field may only contain alphabetic characters.",}}},}TODOsGenerate form object from the serializer definition.Provide utilities for dynamic formsets.Handle unlimited levels of nested serializers?CreditsTools used in rendering this package:Cookiecuttercookiecutter-djangopackageHistory0.2.1 (2020-11-07)File upload now supportsmultipleviaListFileFieldAbility to define different upload URL for each file inputSmall bugs.0.1.1 (2020-08-01)Reactive files objectAdd docs for render_vue_fieldSmall bugs.0.1.0 (2020-07-21)First release on PyPI.